红藕香残玉簟秋。
前情回顾
上篇文章主要大致说了web缓存 及如何配置nginx实现缓存,今天分享一个有关web-component的想法。
基础想法
用 customElements.define();
方法 继承成HTMLElement ,用shadowDom将ui样式定义在组件内,
ui样式用模板字符串单独定义,各个组件自由引用,整体打包为一个
js文件。
好处:
- 项目开发只需引入一个js文件即可。
- 同时支持多种框架
- 样式/主题都可自定义
- 相对于antd-design/iview/element-ui等更加轻量级。
代码示例
项目配置:react-项目
- webpack
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
devtool:'inline-source-map',
mode:"development",
resolve:{
modules:[
path.resolve('./node_modules')
]
},
entry:{
main:'./index.js'
},
output:{
path: path.resolve(__dirname, 'build'),
filename: 'script[name].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loaders:['babel-loader']
},
// {
// test: /\.css|sass|$/,
// loaders:['style-loader','css-loader','sass-loader']
// },
// {
// test: /\.png|jpg|gif$/,
// loader:'url-loader?limit=8192'
// },
// { test: /\.txt$/, use: 'raw-loader' }
]
},
plugins:[
new HtmlWebpackPlugin({
title:'',
template:'./public/index.html',
filename:'index.html',
hash:true,
cache:false
})
]
}
- babel
{
"presets": ["@babel/preset-env","@babel/react"]
}
- package.json
{
"name": "noui",
"version": "1.0.0",
"description": "a ui frame base web component",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --progress --hot --host 0.0.0.0 --port 8089",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"web",
"component",
"ui"
],
"author": "terrence",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.10.2",
"@babel/preset-env": "^7.10.2",
"@babel/preset-react": "^7.10.1",
"babel-loader": "^8.1.0",
"css-loader": "^3.6.0",
"html-webpack-plugin": "^4.3.0",
"node-sass": "^4.14.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"sass-loader": "^8.0.2",
"style-loader": "^1.2.1",
"url-loader": "^4.1.0",
"webpack": "^4.43.0",
"webpack-dev-server": "^3.11.0"
},
"devDependencies": {
"webpack-cli": "^3.3.12"
}
}
- 工具函数
/**
* Camelize a hyphen-delimited string.
*/
const camelizeRE = /-(\w)/g
export const camelize = (str) => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
}
/**
* Capitalize a string.
*/
export const capitalize = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
* Hyphenate a camelCase string.
*/
const hyphenateRE = /\B([A-Z])/g
export const hyphenate =(str) => {
return str.replace(hyphenateRE, '-$1').toLowerCase()
}
- 项目跟页面
import React, { Component } from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
// import { main as mainConfig } from './router/index'
// import { RenderRoutes } from './router/utils'
// import { createStore } from 'redux'
// import { Provider } from 'react-redux'
// import { allReducer } from '@/reducer/reduxs'
// import './App.css'
// import AppButton from './components/button'
// const store = createStore(allReducer)
import components from './components/index'
import {camelize,capitalize,hyphenate} from './utils/tool'
// 遍历组件 在项目中启用 这里可以单独封装
Object.keys(components).forEach(item=>{
console.log(components[item])
if(!customElements.get(hyphenate(item))){
customElements.define(hyphenate(item), components[item][item]);
}
})
class App extends Component {
render() {
return (
// <Provider store={store}>
// <Router>
<div className="App">
<app-button type="primary" id="btn" toggle="true">primary</app-button>
{/* <RenderRoutes routes={mainConfig}/> */}
</div>
// </Router>
// </Provider>
)
}
}
export default App
- button 组件
import Style from './style'
console.log(Style)
class AppButton extends HTMLElement {
//https://mladenplavsic.github.io/css-ripple-effect
static get observedAttributes() { return ['disabled','icon','loading','href','htmltype'] }
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
${Style.buttonStyle}
<${this.href?'a':'button'} ${this.htmltype?'type="'+this.htmltype+'"':''} ${(this.download&&this.href)?'download="'+this.download+'"':''} ${this.href?'href="'+this.href+'" target="'+this.target+'" rel="'+this.rel+'"':''} class="btn" id="btn"></${this.href?'a':'button'}>
<slot></slot>
`
}
focus() {
this.btn.focus();
}
get disabled() {
return this.getAttribute('disabled')!==null;
}
get toggle() {
return this.getAttribute('toggle')!==null;
}
get htmltype() {
return this.getAttribute('htmltype');
}
get name() {
return this.getAttribute('name');
}
get checked() {
return this.getAttribute('checked')!==null;
}
get href() {
return this.getAttribute('href');
}
get target() {
return this.getAttribute('target')||'_blank';
}
get rel() {
return this.getAttribute('rel');
}
get download() {
return this.getAttribute('download');
}
get icon() {
return this.getAttribute('icon');
}
get loading() {
return this.getAttribute('loading')!==null;
}
set icon(value) {
this.setAttribute('icon', value);
}
set htmltype(value) {
this.setAttribute('htmltype', value);
}
set href(value) {
this.setAttribute('href', value);
}
set disabled(value) {
if(value===null||value===false){
this.removeAttribute('disabled');
}else{
this.setAttribute('disabled', '');
}
}
set checked(value) {
if(value===null||value===false){
this.removeAttribute('checked');
}else{
this.setAttribute('checked', '');
}
}
set loading(value) {
if(value===null||value===false){
this.removeAttribute('loading');
}else{
this.setAttribute('loading', '');
}
}
connectedCallback() {
this.btn = this.shadowRoot.getElementById('btn');
this.ico = this.shadowRoot.getElementById('icon');
this.load = document.createElement('xy-loading');
this.load.style.color = 'inherit';
this.btn.addEventListener('mousedown',function(ev){
//ev.preventDefault();
//ev.stopPropagation();
if(!this.disabled){
const { left, top } = this.getBoundingClientRect();
this.style.setProperty('--x',(ev.clientX - left)+'px');
this.style.setProperty('--y',(ev.clientY - top)+'px');
}
})
this.addEventListener('click',function(ev){
if(this.toggle){
this.checked=!this.checked;
}
})
this.btn.addEventListener('keydown', (ev) => {
switch (ev.keyCode) {
case 13://Enter
ev.stopPropagation();
break;
default:
break;
}
})
this.disabled = this.disabled;
this.loading = this.loading;
}
attributeChangedCallback (name, oldValue, newValue) {
if(name == 'disabled' && this.btn){
if(newValue!==null){
this.btn.setAttribute('disabled', 'disabled');
if(this.href){
this.btn.removeAttribute('href');
}
}else{
this.btn.removeAttribute('disabled');
if(this.href){
this.btn.href = this.href;
}
}
}
if( name == 'loading' && this.btn){
if(newValue!==null){
this.shadowRoot.prepend(this.load);
this.btn.setAttribute('disabled', 'disabled');
}else{
this.shadowRoot.removeChild(this.load);
this.btn.removeAttribute('disabled');
}
}
if( name == 'icon' && this.ico){
this.ico.name = newValue;
}
if( name == 'href' && this.btn){
if(!this.disabled){
this.btn.href = newValue;
}
}
if( name == 'htmltype' && this.btn){
this.btn.type = newValue;
}
}
}
// if(!customElements.get('xy-button')){
// customElements.define('xy-button', AppButton);
// }
class AppButtonGroup extends HTMLElement {
static get observedAttributes() { return ['disabled'] }
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
:host {
display:inline-flex;
}
::slotted(xy-button:not(:first-of-type):not(:last-of-type)){
border-radius:0;
}
::slotted(xy-button){
margin:0!important;
}
::slotted(xy-button:not(:first-of-type)){
margin-left:-1px!important;
}
::slotted(xy-button[type]:not([type="dashed"]):not(:first-of-type)){
margin-left:1px!important;
}
::slotted(xy-button:first-of-type){
border-top-right-radius: 0;
border-bottom-right-radius: 0px;
}
::slotted(xy-button:last-of-type){
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
</style>
<slot></slot>
`
}
get disabled() {
return this.getAttribute('disabled')!==null;
}
set disabled(value) {
if(value===null||value===false){
this.removeAttribute('disabled');
}else{
this.setAttribute('disabled', '');
}
}
connectedCallback() {
}
attributeChangedCallback (name, oldValue, newValue) {
}
}
// if(!customElements.get('app-button-group')){
// customElements.define('app-button-group', AppButtonGroup);
// }
export default {
AppButton,
}
- button组件样式
const buttonStyle = `<style>
:host{
position:relative;
display:inline-flex;
padding: .25em .625em;
box-sizing:border-box;
vertical-align: middle;
line-height: 1.8;
overflow:hidden;
align-items:center;
justify-content: center;
border:1px solid var(--borderColor,rgba(0,0,0,.2));
font-size: 14px;
color: var(--fontColor,#333);
border-radius: var(--borderRadius,.25em);
transition:background .3s,box-shadow .3s,border-color .3s,color .3s;
}
:host([shape="circle"]){
border-radius:50%;
}
/*
:host(:not([disabled]):active){
z-index:1;
transform:translateY(.1em);
}
*/
:host([disabled]),:host([loading]){
pointer-events: none;
opacity:.6;
}
:host([block]){
display:flex;
}
:host([disabled]:not([type])){
background:rgba(0,0,0,.1);
}
:host([disabled]) .btn,:host([loading]) .btn{
cursor: not-allowed;
pointer-events: all;
}
:host(:not([type="primary"]):not([type="danger"]):not([disabled]):hover),
:host(:not([type="primary"]):not([type="danger"]):focus-within),
:host([type="flat"][focus]){
color:var(--themeColor,#42b983);
border-color: var(--themeColor,#42b983);
}
:host(:not([type="primary"]):not([type="danger"])) .btn::after{
background-image: radial-gradient(circle, var(--themeColor,#42b983) 10%, transparent 10.01%);
}
:host([type="primary"]){
color: #fff;
background:var(--themeBackground,var(--themeColor,#42b983));
}
:host([type="danger"]){
color: #fff;
background:var(--themeBackground,var(--dangerColor,#ff7875));
}
:host([type="dashed"]){
border-style:dashed
}
:host([type="flat"]),:host([type="primary"]),:host([type="danger"]){
border:0;
padding: calc( .25em + 1px ) calc( .625em + 1px );
}
:host([type="flat"]) .btn::before{
content:'';
position:absolute;
background:var(--themeColor,#42b983);
pointer-events:none;
left:0;
right:0;
top:0;
bottom:0;
opacity:0;
transition:.3s;
}
:host([type="flat"]:not([disabled]):hover) .btn::before{
opacity:.1
}
:host(:not([disabled]):hover){
z-index:1
}
:host([type="flat"]:focus-within) .btn:before,
:host([type="flat"][focus]) .btn:before{
opacity:.2;
}
:host(:focus-within){
/*box-shadow: 0 0 10px rgba(0,0,0,0.1);*/
}
.btn{
background:none;
outline:0;
border:0;
position:
absolute;
left:0;
top:0;
width:100%;
height:100%;
padding:0;
user-select: none;
cursor: unset;
}
xy-loading{
margin-right: 0.35em;
}
::-moz-focus-inner{
border:0;
}
.btn::before{
content: "";
display: block;
position: absolute;
width: 100%;
height: 100%;
left:0;
top:0;
transition:.2s;
background:#fff;
opacity:0;
}
:host(:not([disabled]):active) .btn::before{
opacity:.2;
}
.btn::after {
content: "";
display: block;
position: absolute;
width: 100%;
height: 100%;
left: var(--x,0);
top: var(--y,0);
pointer-events: none;
background-image: radial-gradient(circle, #fff 10%, transparent 10.01%);
background-repeat: no-repeat;
background-position: 50%;
transform: translate(-50%,-50%) scale(10);
opacity: 0;
transition: transform .3s, opacity .8s;
}
.btn:not([disabled]):active::after {
transform: translate(-50%,-50%) scale(0);
opacity: .3;
transition: 0s;
}
xy-icon{
margin-right: 0.35em;
transition: none;
}
:host(:empty) xy-icon{
margin: auto;
}
:host(:empty){
padding: .65em;
}
:host([type="flat"]:empty),:host([type="primary"]:empty){
padding: calc( .65em + 1px );
}
::slotted(xy-icon){
transition: none;
}
:host([href]){
cursor:pointer;
}
</style>`
export default {
buttonStyle
}
思考
- 问题一
样式作为模板字符串,抛出来以后,不利于继承,定制化;但是定制化的样式需求可以通过class 和 style 实现,似乎也并不冲突
- 事件的透传是否需要重新自定义一套事件系统?
PS
只写了几个组件,忙起来又没时间写了。。。
最后说两句
- 动一动您发财的小手,
「点个赞吧」 - 动一动您发财的小手,
「点个在看」 - 都看到这里了,不妨
「加个关注」