前言
原因:闲的。还有一个原因吗?嘿嘿
然后之前就说过了,我拿vue3直接干公司项目了,是内部的孵化项目,客户不多,但是也是正规的生产项目。
技术栈说明:
公司项目:js,element-plus,vue3,jsx,常规template,按需加载(单独领出来是因为这个也会导致你的项目填坑变多)
个人组件库研究项目:TypeScript,vue3,jsx,常规template
主要讲公司项目吧,公司项目上线一个月。还算稳定,不过我需要给没能力填坑的大家看一个截图。
一、vue3生产注意点
1、不能使用getCurrentInstance的ctx
大家在获取app.config.globalProperties.$ELEMENT这里设置的$ELEMENT是要如下
这里的ctx不是steup里面提供的ctx,而是
const { ctx } = getCurrentInstance()
这里ctx在生产环境下是获取不到的,请各位没玩过生产的别误人子弟,这还是我专门给vue3提交issues问来的。
正确应该使用
const { proxy } = getCurrentInstance()
关于在ts中使用
在ts里面大家估计经常会遇到类型定义错误问题。
使用方式1
interface ProxyEx extends ComponentPublicInstance {
$test: number
}
export default defineComponent({
setup(props, ctx) {
const self = getCurrentInstance() as ComponentInternalInstance
const proxy = self.proxy as ProxyEx
console.log(proxy?.$test)
return {}
},
})
使用方式2
上述方案把类型定义到了页面内部,也可以弄一个全局文件去调用。但是总是不方便的。还有就是我们去扩展@vue/runtime-core文件,
然后我们可以在main.ts文件下面增加这段,也不会产生全局错误了
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$test: number
}
}
但是不能放在d.ts文件下面,否则会全局产生错误。我翻了好久的issues也没有找到方案。看官方后续优化解决吧。
二、jsx教学开始
关于jsx的官方教学地址:https://vue3js.cn/docs/zh/guide/render-function.html#jsx
git部分:https://github.com/vuejs/jsx-next/blob/dev/packages/babel-plugin-jsx/README-zh_CN.md
我这里就不放关于h函数的处理了,就直接用和react一样的编码方式来书写。主要是vue3的cli已经内置好了babel插件,也就是说大家不需要单独去引入babel插件。直接可以使用。
也就是大家可以这样写vue文件来书写jsx代码。这里我贴上简单示例
<script lang="tsx">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'test',
emits: [],
directives: {},
props: {},
setup(props, ctx) {
return () => (
<span>简单示例</span>
)
},
})
</script>
Template对比
<template>
<span>简单示例</span>
</template>
<script lang="tsx">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'test',
emits: [],
directives: {},
props: {},
setup(props, ctx) {
return {},
})
</script>
1、注意事项
1、html代码的js文件或者ts文件后缀名增加‘x’,原本是.js变成.jsx,ts变成.tsx
2、.vue文件的script标签增加 lang="tsx",这个呢是你里面存着jsx写法的就需要加。
比如这种情况
<template>
<div class="experiment">
<test-com>
<template #default="de">{{ de }}</template>
</test-com>
<test-a></test-a>
</div>
</template>
<script lang="tsx">
import { defineComponent } from 'vue'
import { TestCom } from './Test'
const TestA = () => <div>fdsafdsa</div>
export default defineComponent({
components: { TestCom, TestA },
setup(props, ctx) {
return {}
},
})
</script>
3、常规jsx语法下是无法配合scpoedCss使用的,解决方法等下就讲
2、在.vue文件下面让你的jsx代码也可以使用scopedCss
也就是大家经常在css部分加的这个<style lang="scss" scoped>
来源:https://github.com/vuejs/jsx-next/issues/51
原理:只要取到 __scopeId, 就可以在 setup/render 函数中
其中可以得到__scopeId的方式
方案1:
// main.js
import { createApp } from 'vue';
import App from './App.vue'
createApp(App(App)).mount('#app') // 这里将 App 传回组件
// ./App.vue
export default ({ __scopeId }) => {
// 这里可以取到 __scopeId
})
方案2:
// ./App.vue
export default {
render(ctx) {
// ctx._.type.__scopeId
}
}
方案3: setup 可以返回一个 render 方法, 用于 render jsx
// ./App.vue
export default {
setup() {
return (ctx) {
// ctx._.type.__scopeId
}
}
}
上面的实现对于大家比较抽象,我这里提供一个自己的实现方式
<script lang="tsx">
import { defineComponent, withScopeId, getCurrentInstance } from 'vue'
export default defineComponent({
setup(props, ctx) {
const instance = getCurrentInstance()
const scopeId = instance.type.__scopeId
const withId = withScopeId(scopeId)
return withId(() => <div class="ceshi">fdsafas</div>)
},
})
</script>
然后附上template对比
<template>
<span>简单示例</span>
</template>
<script lang="tsx">
import { defineComponent} from 'vue'
export default defineComponent({
setup(props, ctx) {},
})
</script>
<style scoped lang="scss">
.ceshi {
width: 100px;
height: 100px;
background: red;
}
</style>
关于像cssModule,css-in-js等方案大家就自行研究。
3、关于class和style样式
这里提醒一下,大家不太会用的直接拿react代码套进来也可以。也就是你可以直接搜索react使用方式,然后这里一样可以用。就是注意下比如className和class这种区别。语法上两者真的差异不大
class部分使用和template部分没有差别
<span class="tooltip" ref={tooltip}>
或者
动态方式就不太一样
<div class={{ 'test-red': classt }}></div>
style部分如果是没有动态的那么和普通的写法没差异
但是动态的需要是这样的
<div style={{ visibility: data.show }}></div>
4、在jsx里面使用ref,reactive等
import { defineComponent, ref, reactive } from 'vue'
const Testref = defineComponent({
setup(props, ctx) {
const dateone = ref('测试')
const datetwo = reactive({
test: '测试',
})
return () => (
<div>
<div>{dateone.value}</div>
<div>{datetwo.test}</div>
</div>
)
},
})
template对比
<template>
<div class="experiment">
<div>{{ dateone }}</div>
<div>{{ test }}</div>
</div>
</template>
<script lang="tsx">
import { defineComponent, reactive, ref, toRefs } from 'vue'
export default defineComponent({
setup(props, ctx) {
const dateone = ref('测试')
const datetwo = reactive({
test: '测试',
})
return {
dateone,
...toRefs(datetwo),
}
},
})
</script>
5、使用插槽和具名插槽
官方:https://vue3js.cn/docs/zh/guide/component-slots.html#插槽内容
const testslot = defineComponent({
setup(props, ctx) {
return () => (
<div>
{/*默认插槽*/}
<div>{ctx.slots.default && ctx.slots.default()}</div>
{/*具名插槽*/}
<div>{ctx.slots.name && ctx.slots.name()}</div>
</div>
)
},
})
template对比
<template>
<div>
<slot></slot>
</div>
<div>
<slot name="name"></slot>
</div>
</template>
<script lang="tsx">
</script>
6、作用域插槽使用
官方地址:https://vue3js.cn/docs/zh/guide/component-slots.html#作用域插槽
传参
const TestA = defineComponent({
setup(props, ctx) {
return () => <div>{ctx.slots.default && ctx.slots.default('作用域传参')}</div>
},
})
使用
const TestD = defineComponent({
setup(props, ctx) {
return () => (
<div>
<span>组件:TestD</span>
<TestA
v-slots={{
default: (n: string) => <span>{n}</span>,
}}
/>
</div>
)
},
})
template对比
传参
<template>
<div>
<slot :defalut="作用域传参"></slot>
</div>
</template>
<script lang="tsx">
</script>
使用
<todo-list>
<template v-slot:default="slotProps">
<span class="green">{{ slotProps}}</span>
</template>
</todo-list>
7、v-if 和 v-for
官方:https://vue3js.cn/docs/zh/guide/render-function.html#v-if-和-v-for
const iffor = defineComponent({
setup(props, ctx) {
const ces = true
const ifelse = () => {
if (ces) return <div>if部分</div>
else return <div>else部分</div>
}
const forarr = () => {
let i = 0
return new Array(10).fill('').map(() => {
i++
return <span>{i}</span>
})
}
return () => (
<div>
{ifelse()}
{forarr()}
</div>
)
},
})
template对比
<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
8、v-model的使用
<input type="text" v-model={inpt.value} onChange={onInput} />
template部分我就不放了,太简单了
9、jsx情况下可以不要.vue文件示例
示例1:
export const TestCom = defineComponent({
setup(props, ctx) {
return () => <TestD />
},
})
示例2:
import { ref } from 'vue'
interface Props {
text: string
}
// ref不能放在组件内部,否则会重新赋值,导致不会响应式变化
const num = ref(1)
const dataone = ref('测试无defineComponent数据变化')
const addnum = () => {
num.value++
}
const inpt = ref('')
const onInput = () => {
console.log(inpt.value)
}
const NoDcom = (props: Props, ctx: any) => {
//console.log(props, ctx)
return (
<div>
<div>{dataone.value}</div>
<button onClick={addnum}>点击测试数据变化</button>
<div>变化的数字{num.value}</div>
<input type="text" v-model={inpt.value} onChange={onInput} />
</div>
)
}
export default NoDcom
10、关于在vue文件组件注册
注意jsx的文件就不要加jsx后缀了
使用和常规的组件注册一样
import { TestCom } from './Test'
export default defineComponent({
components: { TestCom},
setup(props, ctx) {
return {}
},
})
</script>
11、关于click事件之类
原本模板中是@click,全部改为onClick模式,和react比较相似
举例
<input type="text" v-model={inpt.value} onInput={onInput} onChange={onInput} />
template
<input type="text" v-model="inpt.value" @input="onInput" @change="onInput" />
12、使用Teleport
在jsx下使用vue内置的组件需要额外导入组件,并且名称不能变。
import { defineComponent, Teleport } from 'vue'
// 使用Teleport
const teleportCum = defineComponent({
setup(props, ctx) {
return () => (
<Teleport to="body">
<div>teleport值</div>
</Teleport>
)
},
})
Template使用
并且是不需要单独导入,也没有大小写区分的问题
<template>
<teleport></teleport>
</template>
13、使用Transition
和使用teleport一样,就不单独写代码了。上面的例子一样,把Teleport换成Transition就可以跑了
14、关于指令修饰符
官方自带的指令修饰符方面是说类似自己实现
地址:https://v3.cn.vuejs.org/guide/render-function.html#v-on
然后我个人在项目中因为自定义了一个可以随意拖动的v-move指令,并且定义了boundary修饰符代表是否可以只在父元素范围内移动。这时候就很烦了
最后多方百度下至少自定义的指令修饰符是通过“_”连接,如下
<div class="dht-float-win" v-dht-move_boundary={{ callmove: startmove, callstop: stopmove }}>
{ctx.slots.default && ctx.slots.default()}
</div>
而template里面
<div class="dht-float-win" v-dht-move.boundary="{ callmove: startmove, callstop: stopmove }">
<slot></slot>
</div>
作者:海天酱油_沃利贝尔
链接:https://juejin.cn/post/6911883529098002446/
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
三、关于jsx和常规的sfc(vue文件方式)
写到这里,说实在我有点想不到该写什么。jsx给我的感觉就是在用react语法。为什么呢?因为这个babel插件是阿里团队优先搞出来了,然后大家都知道阿里是react技术栈为主的。所以语法上面靠近react。
我说说我的几个见地吧
1、jsx并没有更加方便,只是让你更接近vue模板渲染的底层原理
2、jsx语法灵活很多,光我上面就出现了两种jsx书写方式,个人还是推荐defineComponent方式,更加是一个整体
3、jsx书写会比较强迫的让你把for循环等小逻辑模板单独成为一个函数,优点就是颗粒度更小了,和sfc书写方式相比,没有那么方便。
特别是什么呢,我上面的提供方式优点不方便在return部分写逻辑,如果你写if会报错,官方方面都是用的render方式,其实两者的差异并不大
4、如果你用vue那么jsx可以作为一种优化方式(官方推荐使用template方式意味着,template方式bug会优先解决),而不是大规模使用。特别是中小公司,大公司也好不到那里去。除了业内头部公司,有几个团队能完全规范化的呢?
这里我说一句话:何不食肉糜?
要是全用jsx,那我还是用react吧。何况react薪资更高呢
5、jsx一定要学,但是ts更要学。虽然未来一定不是ts的天下(浏览器领域),但是ts所带来的影响已经把中高端领域覆盖。
6、学习jsx是让你拓宽视野,react生态毕竟是领头羊。
四、关于vue3书写方式,我想新手很容易写出这种代码吧。
放一个我个人封装的组件地址:https://github.com/ht-sauce/visualization/tree/master/src/componentsLibrary/Popper
文档地址:https://www.yuque.com/cv8igf/oy3c8b/uy5sqv
这恐怕就是大佬所担心的面条式代码,说白了,这就是vue2所带来的习惯。vue3的情况下,那么props,函数,响应式部分都是可以抽离到单独的js文件,但是vue2恐怕只能抽离props部分了。
<script lang="tsx">
import { dataType, visibility } from './types'
import { defineComponent, onMounted, ref, reactive, onUnmounted, watch } from 'vue'
import { createPopper } from '@popperjs/core'
import type { Instance, Options } from '@popperjs/core'
import ClickOutside from '../ClickOutside'
export default defineComponent({
name: 'DhtPopper',
emits: ['update:modelValue', 'hide', 'show'],
directives: {
'dht-click-outside': ClickOutside.directive,
},
props: {
trigger: {
type: String,
default: 'hover', // hover,click,manual
},
modelValue: Boolean, // 手动绑定
disabled: {
type: Boolean,
default: false,
},
arrow: {
type: Boolean,
default: true,
},
offset: {
type: Number,
default: 16,
},
placement: {
type: String,
default: 'bottom',
},
options: {
type: Object,
default: () => null, // 出现ts问题参考:https://github.com/vuejs/vue-next/issues/2474
},
clickOutside: {
type: Boolean,
default: true,
},
},
setup(props, ctx) {
let popperInstance: Instance | null = null
const data = reactive({
show: 'hidden',
} as dataType)
// 被绑定的
const popper = ref<string | HTMLElement>('popper')
// 会移动的,这是提示内容
const tooltip = ref<string | HTMLElement>('tooltip')
// 合并参数
function mergeOptions() {
const opt = {
placement: props.placement,
...props.options,
} as Options
const modifiers = []
modifiers.push({
name: 'offset',
options: {
offset: [0, props.offset],
},
})
opt.modifiers = [...modifiers]
return opt
}
onMounted(() => {
if (props.disabled) return false
const popperDom = popper.value as HTMLElement
const tooltipDom = tooltip.value as HTMLElement
popperInstance = createPopper(popperDom, tooltipDom, mergeOptions())
})
onUnmounted(() => {
;(popperInstance as Instance)?.destroy()
popperInstance = null
})
function hide() {
data.show = visibility.hidden
ctx.emit('update:modelValue', false)
ctx.emit('hide')
}
function show() {
data.show = visibility.visible
ctx.emit('update:modelValue', true)
ctx.emit('show')
}
// 点击事件
function onClick() {
if (props.trigger !== 'click') return null
if (data.show === 'hidden') show()
else hide()
}
function onMouseover() {
if (props.trigger !== 'hover') return null
show()
}
function onMouseout() {
if (props.trigger !== 'hover') return null
hide()
}
watch(
() => props.modelValue,
(e) => {
if (props.trigger !== 'manual') return null
if (e) show()
else hide()
},
)
function clickOutside() {
if (props.trigger === 'hover') return
if (data.show === 'hidden') return
if (props.clickOutside) hide()
}
return () => (
<span v-dht-click-outside={clickOutside} class="dht-popper">
<span ref={popper} onClick={onClick} onMouseover={onMouseover} onMouseout={onMouseout}>
{ctx.slots.default && ctx.slots.default()}
</span>
<span class="tooltip" style={{ visibility: data.show }} ref={tooltip}>
{props.arrow && <span class="arrow" data-popper-arrow />}
{ctx.slots.tooltip && ctx.slots.tooltip()}
</span>
</span>
)
},
})
</script>
五、致谢
谢谢大家观看。喜欢的点个赞。