v-r-t's Introduction
v-r-t's People
v-r-t's Issues
webpack 使用记录
Node 模块的 polyfill
在引入 Vue 时,webpack 会很好心的帮我们引入几个垫片库 /buildin/global.js
, ./node_modules/setimmediate/setImmediate.js
, ./node_modules/timers-browserify/main.js
, ./node_modules/process/browser.js
, 为啥呢?
官方文档:
这些选项可以配置是否 polyfill 或 mock 某些 Node.js 全局变量和模块。这可以使最初为 Node.js 环境编写的代码,在其他环境(如浏览器)中运行。
之前使用场景都是浏览器,所以这样的 polyfill 是可行的,但是在当前的环境中是不可行的。
解决:
// webpack config
node: false
官方文档:Node
`updateElement`
updateElement
// 原始样式
{
style: {
color: 'blue',
background-color: 'white',
flex: 2,
line: 3
}
}
// 向 native 发送 update 请求
{
style: {
color: 'red', // 改变 color
background-color: '' // 置空,表示删除
border:1 // 新增
}
}
prepare
-
Viola-js
-
framework
-
tools
-
-
Viola-Vue
- how
-
Loader
-
viola-style-loader
-
typescript
-
-
Cli
- peerDependencis
在 vue-loader V14 中 style 的提取(json化)
vue-loader v14 中对 style 的载入是一个动态的载入
一个 .vue 文件经过 vue-loader 后,style 会是一个注入函数
var injectStyle = require("!!vue-style-loader!css-loader!../node_modules/vue-loader/lib/style-compiler/index?{\"optionsId\":\"0\",\"vue\":true,\"id\":\"data-v-dc790388\",\"scoped\":true,\"sourceMap\":false}!sass-loader!../node_modules/vue-loader/lib/selector?type=styles&index=0!./hello.vue")
injectStyle 是经过 selector
, style-compiler
, css-loader
, vue-style-loader
后生成的一个注入函数,随后被作为参数传入了 normalizeComponent()
normalizeComponent
而关于 normalizeComponent
, 文件里的注释如下,说的是这个函数是属于 runtime 的,打包后才运行的
This module is a runtime utility for cleaner component module output and will
be included in the final webpack user bundle.
normalizeComponent
对 style 的处理:
- 第一步,生成一个样式加载的函数
/* 样式加载函数 */
var hook
if (moduleIdentifier) { // server build
hook = function (context) {
// 2.3 injection
context =
context || // cached call
(this.$vnode && this.$vnode.ssrContext) || // stateful
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
// 2.2 with runInNewContext: true
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
context = __VUE_SSR_CONTEXT__
}
// inject component styles
if (injectStyles) {
injectStyles.call(this, context)
}
// register component module identifier for async chunk inferrence
if (context && context._registeredComponents) {
context._registeredComponents.add(moduleIdentifier)
}
}
// used by ssr in case component is cached and beforeCreate
// never gets called
options._ssrRegister = hook
} else if (injectStyles) {
hook = shadowMode
? function () { injectStyles.call(this, this.$root.$options.shadowRoot) }
: injectStyles
}
- 第二步,样式加载函数作为钩子函数执行
if (hook) {
console.log('\n\n options: ', options)
if (options.functional) {
// for template-only hot-reload because in that case the render fn doesn't
// go through the normalizer
options._injectStyles = hook
// register for functioal component in vue file
var originalRender = options.render
options.render = function renderWithStyleInjection (h, context) {
hook.call(context)
return originalRender(h, context)
}
} else {
// inject component registration as beforeCreate hook
var existing = options.beforeCreate
options.beforeCreate = existing
? [].concat(existing, hook)
: [hook]
}
}
生成 loaderString
生成一个 require
的 string
,用来加载style
// todo
selector
,
selector
是 loaderString
中的第一个 loader,用来选择 .vue
文件的对应代码块
// todo
style-compiler
style-compiler
// todo
vue-style-loader
vue-style-loader
生成动态插入样式的代码
// todo
关于如何自动执行 `createBody`
new Vue( el: '#app')
执行 new Vue( el: '#app')
的最后一步就是:替换 #app 的 dom节点
替换通过 'appendChild' 和 ‘removeChild’ 来实现。
appendChild
: append 的是 vue 帮我们创建的子树removeChild
: remove 的是 原本存在 Dom Tree 的节点
document
我们初始的 document 有一个子节点 body,这个 body 也是一个 Element ( 只是 ref 为 ‘root’,以标志为渲染树的根节点 ),也就是我们在 query 中可以返回 document.body,最后就是 document
append 一个子树,因此加上 document.appendChild
, document.removeChild
方法
document.appendChild
: append 的是一个 body,但是 document 应当只有一个 body,所以此时应该是替换操作
appendChild (body) {
if (body instanceof Element) {
body.ref = BODY_REF
this.body = body
this.render()
}
}
document.removeChild
: remove 了 body 的操作似乎没有意义,直接return false
在执行 appendChild(body)
时,进行 render
的操作,进行一整颗树的渲染,(注意: 此后的更新是局部更新)
SFC 转化结果怎么转换成实例
weex-loader:
var __vue_exports__, __vue_options__
var __vue_styles__ = []
/* styles */
__vue_styles__.push(require("!!../node_modules/weex-vue-loader/lib/style-loader!../node_modules/weex-vue-loader/lib/style-rewriter?id=data-v-dc790388!../node_modules/weex-vue-loader/lib/selector?type=styles&index=0!./hello.vue")
)
// __vue_styles__ 为一个数组
// 格式为:
// [
// {
// className: {
// ...style
// },
// className: {}
// },
// { ... }, ...
// ]
/* script */
__vue_exports__ = require("!!../node_modules/weex-vue-loader/lib/script-loader!../node_modules/weex-vue-loader/lib/selector?type=script&index=0!./hello.vue")
/* template */
var __vue_template__ = require("!!../node_modules/weex-vue-loader/lib/template-compiler?id=data-v-dc790388!../node_modules/weex-vue-loader/lib/selector?type=template&index=0!./hello.vue")
__vue_options__ = __vue_exports__ = __vue_exports__ || {}
if (
typeof __vue_exports__.default === "object" ||
typeof __vue_exports__.default === "function"
) {
if (Object.keys(__vue_exports__).some(function (key) { return key !== "default" && key !== "__esModule" })) {console.error("named exports are not supported in *.vue files.")}
__vue_options__ = __vue_exports__ = __vue_exports__.default
}
if (typeof __vue_options__ === "function") {
__vue_options__ = __vue_options__.options
}
__vue_options__.__file = "D:\\Viola\\webpack-v\\v-r-t\\webpack-test\\src\\hello.vue"
__vue_options__.render = __vue_template__.render
__vue_options__.staticRenderFns = __vue_template__.staticRenderFns
__vue_options__._scopeId = "data-v-8229fd3a"
__vue_options__.style = __vue_options__.style || {}
__vue_styles__.forEach(function (module) {
for (var name in module) {
__vue_options__.style[name] = module[name]
}
})
/*
循环将 所有的 class-style 保持在 __vue_options__.style
格式为:
{
className: { ...style },
className: { ...style }
}
后续可通过 vnode.ctx.$options.style 获取到,但设置 className 时抽取 style 加上 (setStyle)
*/
if (typeof __register_static_styles__ === "function") {
__register_static_styles__(__vue_options__._scopeId, __vue_styles__)
}
module.exports = __vue_exports__
重要过程:
- 配置 转换成一个构造器,继承于 Vue,接着作为 new Vue( ) 的参数之一, vnode.componentOptions.Ctor
function createComponentInstanceForVnode()
中 return new vnode.componentOptions.Ctor(options)
weex 的 style 转换
-
Vue-template-compiler 中的
parse
方法将style 部分提出来 -
Weex-style-rewriter
通过 postCSS 转换为标准css样式表
-
style-loader
通过
genStyleString
方法转化为 jsongenStyleString
使用的是 weex-styler -
weex-styler
- 引用 npm 包 css
parse
方法中css.parse()
转换为了 CSS AST- 解析 AST,转化为 style object
Viola-style-loader
Viola-style-loader 的 StyleDescriptior
StyleDescriptior
StyleDescriptior
用来描述一个样式规则的数据结构,目前结构如下(可能后续会改动):
class StyleDescriptor {
constructor (
style = {},
scoped_id = '',
state = {},
attrs = {},
children = []
) {
this.style = style
this.scoped_id = scoped_id
this.state = state
this.attrs = attrs
this.children = children
}
}
style
: 当前这个选择器(如 .container)的样式scoped_id
: 组件局域样式 idattrs
: 属性选择器state
: 伪类children
: 子选择器
一个最简单的例子可以表示如下:
/* scoped_id: data-v-sf023154 */
.viola {
color: red;
}
.viola[type=number] {
background-color: white;
}
.viola:hover {
color: blue
}
用 StyleDescriptor
表示如下:
{
viola: {
style: {
color: '#ff0000'
},
scoped_id: 'data-v-sf023154',
state: {
hover: {
color: 'blue'
}
},
attrs: {
type: {
number: {
backgroundColor: 'white'
}
}
}
}
}
但是,目前对伪类的样式表示暂时为下面(所以上面说,可能结构会改动):
{
viola: {
style: {
'color': '#ff0000', // 普通状态
'color:hover': 'blue' // hover 状态
}
}
因为对于伪类的样式表示应该由 native 端来维护,如果由 web 端维护的话,就意味着,web 端需要 native 来通知是否触发伪类,再传递相应的样式改变过去,这期间就多了两次通讯了。
所以, 我觉得应该要用一种形式 (比如上面的示例) 区分开样式,传递给 native ,由 native 进行维护。
在 Vue 中的获取
通过 Viola-style-loader
后,我们将当前组件的样式作为一个 $options
的配置项附在组件上。
// loader inject style
_options._stylesheet = injectStyle
// vue get style
vnode.context.$options._stylesheet
Vue 使用 updateClass
来更新对于 vnode
的 class
, 那么我们就可以在此时更新的同时,将 class
转换为相应的 style
, 然后 setStyle
来更新类样式.
注意:此时还可以进行属性选择器的样式追加
/**
* 由类名获取样式
* @return { ...style }
**/
function getStyle(classList, vnode) {
let res = {},
stylesheet = vnode.context.$options._stylesheet
classList.reduce((res, className) => {
let styleDescriptor = stylesheet[className]
if (styleDescriptor) {
// 融合 类名选择器的样式
extend(res, styleDescriptor.style)
// todo attr selector 融合属性选择器的样式
if (styleDescriptor.attrs) {
let attrStyle = styleDescriptor.attrs,
vnodeAttr = vnode.data.attrs
if (isEmptyObj(vnodeAttr)) return
for (const k in attrStyle) {
let vnodeAttrValue = vnodeAttr[k],
attrStyleCollection = attrStyle[k]
if (isDef(vnodeAttrValue)) {
const value = attrStyle[k]
console.log('有属性样式~~', vnodeAttr[k], value)
}
}
}
}
}, res)
return res
}
Viola-style-loader 的 css
转换
对于样式的转换过程,会调用到以下一系列的 loader,viola-style-loader 是最后一个 loader。
viola-style-loader!style-compiler!sass-loader/lib/loader.js!selector.js
经过前面的 loader ,最终会给 Viola-style-loader 传递的是一份样式表代码:
<!-- 普通类名 -->
.viola[data-v-dc790388] {
background-color: #4d45be;
transform: scale(1.5);
}
<!-- 后代选择器 -->
.viola .p[data-v-dc790388] {
color: blue;
}
<!-- 属性选择 -->
.viola[type=oj][data-v-dc790388] {
color: red;
}
<!-- 伪类 -->
.viola[data-v-dc790388]:hover {
width: 300;
}
<!-- 字体 -->
@font-face {
font-family: "Bitstream Vera Serif Bold";
src: url("http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf");
}
然后,我们需要对它进行转换,在这里我们使用的是 npm 包 css
(github 地址). css
可以将普通的样式表解析为一个语法树(style AST
)。
// 基本使用如下
var css = require('css')
var AST = css.parse(cssCode)
如我们上面的 css 代码会转换为的 AST 大致如下( 部分省略,具体规则:github 地址 ):
{
"type": "stylesheet",
"stylesheet": {
"rules": [
{
"type": "rule",
"selectors": [
".viola[data-v-dc790388]"
],
"declarations": [
{
"type": "declaration",
"property": "background-color",
"value": "#4d45be",
"position": {
"start": {
"line": 3,
"column": 3
},
"end": {
"line": 3,
"column": 28
}
}
},
],
"position": {
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 5,
"column": 2
}
}
},
{
"type": "font-face",
"declarations": [
{
"type": "declaration",
"property": "font-family",
"value": "\"Bitstream Vera Serif Bold\"",
"position": {
"start": {
"line": 16,
"column": 3
},
"end": {
"line": 16,
"column": 43
}
}
},
{
"type": "declaration",
"property": "src",
"value": "url(\"http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf\")",
"position": {
"start": {
"line": 17,
"column": 3
},
"end": {
"line": 17,
"column": 78
}
}
}
],
"position": {
"start": {
"line": 15,
"column": 1
},
"end": {
"line": 18,
"column": 2
}
}
}
],
"parsingErrors": []
}
}
通过上面的例子分析可以得到几个重要的点:
-
type
:stylesheet
,rule
,font-face
,declaration
具有不同的 type 来区分节点类型
-
selectors
选择器数组,但是里面只有一个值,对应选择器(如:
.viola[data-v-dc790388]:hover
) -
property
和value
对应属性和值
那么我们暂且可以根据这个来进行转换了。
// 定义常量(部分)
const AST_TYPE = {
STYLESHEET: 'stylesheet',
RULE: 'rule',
DECLARATION: 'declaration',
FONT_FACE: 'font-face',
IMPORT: 'import',
KEYFRAME: 'keyframe'
}
// 定义解析正则表达式 (部分)
const REG_EXP = {
CLASS: /^\./,
SELECT_TYPE: /^[\.#]/,
// [data-v-[hash]]
SCOPE_ID_FIELD: /\[(data-v-[a-z0-9]+)\]/,
// data-v-[hash]
SCOPE_ID: /(?:data-v-[a-z0-9]+)$/,
// selector: .class[attr]:state $1: class, $2: attr, $3: state
SELECTOR: /^(?:\.?([A-Za-z0-9_\-]+))(?:\[([A-Za-z0-9=\-]+)\])?(:[a-z:]+)?$/,
// selector: .class[attr]:state $1: class, $2: attr, $3: state
// SELECTOR: /^(\.?[A-Za-z0-9_\-]+)(?:\[([A-Za-z0-9=\-]+)\])?(:[a-z:]+)$/,
KEY_VALUE: /^([a-zA-Z]+)=([a-zA-Z0-9]+)$/
}
下面是转换的部分代码,目的是转换为用 StyleDescriptior
表示, 后续会改动,暂先做个记录:
function walkAST(cssAST) {
if (!cssAST) return
if (
cssAST.type === AST_TYPE.STYLESHEET
&& cssAST.stylesheet
&& cssAST.stylesheet.rules
) {
let sheet = cssAST.stylesheet // 样式表
let rules = sheet.rules // rule array
let styles = {} // 收集样式
rules.forEach((rule) => {
let styleDescription = {}
if (rule.type === AST_TYPE.RULE) {
let declarations = rule.declarations
// selector 是选择器,rule.selectors 为数组
let selector = Array.isArray(rule.selectors) ? rule.selectors[0] : rule.selectors
let selectorName
let selectorsPart = selector.split(' ')
switch (selectorsPart.length) {
case 1:
// selectorName = REG_EXP.CLASS.test(selector) ? selector.slice(1) : selector
selectorName = selector
break;
case 2:
let _name = selectorsPart[1]
// selectorName = REG_EXP.CLASS.test(selector) ? selector.slice(1) : selector
selectorName = _name
default:
break;
}
let {
className, scoped_id, attr, state, stateArr
} = getSelectorData(selectorName)
_c.log('getSelectorData', {
selectorName,
className, scoped_id, attr, state, stateArr
})
let cur = styles[className] || (styles[className] = new StyleDescriptor()), target
cur.scoped_id = scoped_id
if (cur && attr[0]) {
let key = attr[0]
let _targetAttr = cur.attrs[key] || (cur.attrs[key] = {})
// target = attr[1]
// ? (cur[attr[0]] && cur[attr[0]][attr[1]]) || ((cur[attr[0]] = {}) && (cur[attr[0]][attr[1]] = {}))
// : cur[attr[0]] || (cur[attr[0]] = {})
target = attr[1]
? (_targetAttr[attr[1]] || (_targetAttr[attr[1]] = {}))
: _targetAttr
} else {
target = cur.style
}
declarations.reduce((result, style) => {
// todo: property , value filter(transform)
result[style.property + state] = style.value
return result
}, target)
}
})
return styles
}
}
function getSelectorData(selector) {
let selectorData = {
className: null,
scoped_id: '',
attr: [],
state: '',
stateArr: [],
}
selector = selector.replace(REG_EXP.SCOPE_ID_FIELD, (match, $1) => {
selectorData.scoped_id = $1
return ''
})
let matchResult = REG_EXP.SELECTOR.exec(selector)
if (matchResult) {
// class
selectorData.className = matchResult[1]
let attr = matchResult[2]
if (attr) {
// [data-v-hash]
if (REG_EXP.SCOPE_ID.test(attr)) {
selectorData.scoped_id = attr
} else {
// attr: [key, vaule]
// ['type', 'number']
// ['disable']
selectorData.attr = attr.split('=')
// isDef(selectorData.attr[2]) || (selectorData.attr[2] = true)
// selectorData[attrKeyVaule[0]] = isDef(attrKeyVaule[1]) ? attrKeyVaule[1] : true
}
}
if (matchResult[3]) {
// state: ':hover:active'
selectorData.state = matchResult[3]
// stateArr: ['hover', 'active']
selectorData.stateArr = selectorData.state.slice(1).split(':')
}
}
return selectorData
}
/**
* callNative
* @param module 调用模块,如 dom,jsapi
* @param action 触发的 action,如 createBody, addElement
* @param payload 负载参数,对应不同action,参数对象里的格式也是不一样的
*
**/
callNative('dom', 'createBody', {...domObj})
总结
-
StyleDescriptior
需要再根据具体的进行 update如上面提到的伪类表示。
用这个结构可以进行 属性选择, 应该也是可以进行 后代选择 的,后续需要评估下有没有这个必要性
-
样式需要过滤转换
如
color: red
应该转换为color: '#ff0000'
等 -
可能可以优化点:
vue-loader 和 weex-loader 的做法都是抽出样式,然后在 vue 的 runtime 进行融合
但是不同的是!!
- vue-loader 抽取后的运行环境是 浏览器,样式的加入是动态插入 style 标签;
- weex 运行的不是 浏览器,没有 css stylesheet 之类的概念,只能在运行时中做样式融合
- 不同就在,后一种的处理是不是会比第一种慢上不少,且支持的写法不多??
有没有可能在 loader 这一层就将样式附加上了??
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.