前端图床搭建实践(前端)

简介: 前端开发过程中不可避免会用到图片、视频等多媒体物料,常见的处理方案通常会进行动静分离,将图片等资源放置在图床上,除了使用业界常用的图床资源,比如:七牛云、微博图床等,除了借助第三方图床外,我们也可以自己搭建一个图床,为团队业务开发提供更好的基础服务,提升开发体验及效率。本文旨在回顾总结下自建图床的前端部分实现方案,希望能够给有类似需求的同学一些借鉴和方案。

前端 | 前端图床搭建实践(前端).png

项目背景

前端开发过程中不可避免会用到图片、视频等多媒体物料,常见的处理方案通常会进行动静分离,将图片等资源放置在图床上,除了使用业界常用的图床资源,比如:七牛云、微博图床等,除了借助第三方图床外,我们也可以自己搭建一个图床,为团队业务开发提供更好的基础服务,提升开发体验及效率。本文旨在回顾总结下自建图床的前端部分实现方案,希望能够给有类似需求的同学一些借鉴和方案。

方案

前端部分架构选型,考虑到Vue3即将成为主版本,作为前端基建侧的应用,考虑想要使用Vue3全家桶来进行前端侧的相关实现,这里使用了vite(vue-template-ts)+vue3+vuex@next+vue-router@next的使用方案,也为vite的打包构建进行一步的技术预(cai)研(keng)。(ps:vite确实快,但是目前直接上工业环境还需要考量,还有不少坑,个人认为跨语言的前端工程化可能会是后续前端工程化的发展方向)

目录

  • src

    • assets
    • components

      • index.ts
      • Card.vue
      • Login.vue
      • Upload.vue
      • WrapperLayouts.vue
      • WrapperLogin.vue
      • WrapperUpload.vue
    • config

      • index.ts
      • menuMap.ts
      • routes.ts
    • layouts

      • index.ts
      • Aside.vue
      • Layouts.vue
      • Main.vue
      • Nav.vue
    • route

      • index.ts
    • store

      • index.ts
    • utils

      • index.ts
      • reg.ts
      • validate.ts
    • views

      • Page.vue
    • App.vue
    • index.scss
    • main.ts
    • vue-app-env.d.ts
  • index.html
  • tsconfig.json
  • vite.config.ts

实践

前端图床涉及到权限验证,对于获取图片不进行认证确认,而对于需要进行上传及删除图片操作会需要进行登录鉴权

源码

vue3中可以通过class以及template两种方案来书写,使用composition-api的方案,个人建议还是使用class-component更加舒服,也更像react的写法,这里夹杂使用了composition-api和options-api的使用,目前vue是兼容的,对于从vue2中过来的同学,可以逐步去适应composition-api的写法,然后逐步按照hooks的函数式的思路去进行前端的业务实现

vite.config.ts

vite构建相关的一些配置,可以根据项目需求进行环境配置

const path = require('path')
// vite.config.js # or vite.config.ts
console.log(path.resolve(__dirname, './src'))

module.exports = {
  alias: {
    // 键必须以斜线开始和结束
    '/@/': path.resolve(__dirname, './src'),
  },
  /**
   * 在生产中服务时的基本公共路径。
   * @default '/'
   */
  base: './',
  /**
   * 与“根”相关的目录,构建输出将放在其中。如果目录存在,它将在构建之前被删除。
   * @default 'dist'
   */
  outDir: 'dist',
  port: 3000,
  // 是否自动在浏览器打开
  open: false,
  // 是否开启 https
  https: false,
  // 服务端渲染
  ssr: false,
  // 引入第三方的配置
  //   optimizeDeps: {
  //     include: ["moment", "echarts", "axios", "mockjs"],
  //   },
  proxy: {
    // 如果是 /bff 打头,则访问地址如下
    '/bff/': {
      target: 'http://localhost:30096/',// 'http://10.186.2.55:8170/',  
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/bff/, ''),
    }
  },
  optimizeDeps: {
    include: ['element-plus/lib/locale/lang/zh-cn', 'axios'],
  },
}

Page.vue

每个子项目页面的展示,只需要一个组件,进行不同的数据渲染即可

<template>
  <div class="page-header">
    <el-row>
      <el-col :span="12">
        <el-page-header
          :content="$route.fullPath.split('/').slice(2).join(' > ')"
          @back="handleBack"
        />
      </el-col>
      <el-col :span="12">
        <section class="header-button">
          <!-- <el-button class="folder-add" :icon="FolderAdd" @click="handleFolder" >新建文件夹</el-button> -->
          <el-button class="upload" :icon="Upload" type="success" @click="handleImage">上传图片</el-button>
        </section>
      </el-col>
    </el-row>
  </div>
  <div class="page">
    <el-row :gutter="10">
      <el-col v-for="(item, index) in cards" :xs="12" :sm="8" :md="6" :lg="4" :xl="4">
        <Card
          @next="handleRouteView(item.ext, item.name)"
          @delete="handleDelete"
          :name="item.name"
          :src="item.src"
          :ext="item.ext"
          :key="index"
        />
      </el-col>
    </el-row>
    <el-pagination
      layout="sizes, prev, pager, next, total"
      @size-change="handleSizeChange"
      @current-change="handlePageChange"
      :current-page.sync="pageNum"
      :page-size="pageSize"
      :total="total"
    ></el-pagination>
    <router-view />
  </div>
  <WrapperUpload ref="wrapper-upload" :headers="computedHeaders" />
  <WrapperLogin ref="wrapper-login" />
</template>

<script lang="ts">
import {
  defineComponent,
} from 'vue';
import { useRoute } from 'vue-router'
import {
  FolderAdd,
  Upload
} from '@element-plus/icons-vue'

import { Card, WrapperUpload, WrapperLogin } from '../components'

export default defineComponent({
  name: 'Page',
  components: {
    Card,
    WrapperUpload,
    WrapperLogin
  },
  props: {

  },
  setup() {
    return {
      FolderAdd,
      Upload
    }
  },
  data() {
    return {
      cards: [],
      total: 30,
      pageSize: 30,
      pageNum: 1,
      bucketName: '',
      prefix: '',

    }
  },
  watch: {
    $route: {
      immediate: true,
      handler(val) {
        console.log('val', val)
        if (val) {
          this.handleCards()
        }
      }
    }
  },
  methods: {
    handleBack() {
      this.$router.go(-1)
    },
    handleFolder() {

    },
    handleDelete(useName) {
      console.log('useName', useName)
      const [bucketName, ...objectName] = useName.split('/');
      console.log('bukcetName', bucketName);
      console.log('objectName', objectName.join('/'));
      if (sessionStorage.getItem('token')) {
        this.$http.post("/bff/imagepic/object/removeObject", {
          bucketName: bucketName,
          objectName: objectName.join('/')
        }, {
          headers: {
            'Authorization': sessionStorage.getItem('token'),
          }
        }).then(res => {
          console.log('removeObject', res)
          if (res.data.success) {
            this.$message.success(`${objectName.pop()}图片删除成功`);
            setTimeout(() => {
              this.$router.go(0)
            }, 100)

          } else {
            this.$message.error(`${objectName.pop()}图片删除失败,失败原因:${res.data.data}`)
          }
        })
      } else {
        this.$refs[`wrapper-login`].handleOpen()
      }
    },
    handleImage() {
      sessionStorage.getItem('token')
        ? this.$refs[`wrapper-upload`].handleOpen()
        : this.$refs[`wrapper-login`].handleOpen()
    },
    handleRouteView(ext, name) {
      // console.log('extsss', ext)
      if (ext == 'file') {
        console.log('$router', this.$router)

        console.log('$route.name', this.$route.name, this.$route.path)


        this.$router.addRoute(this.$route.name,
          {
            path: `:${name}`,
            name: name,
            component: () => import('./Page.vue')
          }
        )

        console.log('$router.options.routes', this.$router.options.routes)


        this.$router.push({
          path: `/page/${this.$route.params.id}/${name}`
        })
      } else {

      }
    },
    handlePageChange(val) {
      this.pageNum = val;
      this.handleCards();
    },
    handleSizeChange(val) {
      this.pageSize = val;
      this.handleCards();
    },
    handleCards() {
      this.cards = [];
      let [bucketName, prefix] = this.$route.path.split('/').splice(2);
      this.bucketName = bucketName;
      this.prefix = prefix;
      console.log('bucketName', bucketName, prefix)
      this.$http.post("/bff/imagepic/object/listObjects", {
        bucketName: bucketName,
        prefix: prefix ? prefix + '/' : '',
        pageSize: this.pageSize,
        pageNum: this.pageNum
      }).then(res => {
        console.log('listObjects', res.data)
        if (res.data.success) {
          this.total = res.data.data.total;
          if (prefix) {
            this.total -= 1;
            return res.data.data.lists.filter(f => f.name != prefix + '/')
          }
          return res.data.data.lists
        }
      }).then(data => {
        console.log('data', data)
        data.forEach(d => {
          // 当前目录下
          if (d.name) {
            this.$http.post('/bff/imagepic/object/presignedGetObject', {
              bucketName: bucketName,
              objectName: d.name
            }).then(url => {
              // console.log('url', url)
              if (url.data.success) {
                const ext = url.data.data.split('?')[0];
                // console.log('ext', ext)
                let src = '', ext_type = '';
                switch (true) {
                  case /\.(png|jpg|jpeg|gif|svg|webp)$/.test(ext):
                    src = url.data.data;
                    ext_type = 'image';
                    break;
                  case /\.(mp4)$/.test(ext):
                    src = 'icon_mp4';
                    ext_type = 'mp4';
                    break;
                  case /\.(xls)$/.test(ext):
                    src = 'icon_xls';
                    ext_type = 'xls';
                    break;
                  case /\.(xlsx)$/.test(ext):
                    src = 'icon_xlsx';
                    ext_type = 'xlsx';
                    break;
                  case /\.(pdf)$/.test(ext):
                    src = 'icon_pdf';
                    ext_type = 'pdf';
                    break;
                  default:
                    src = 'icon_unknow';
                    ext_type = 'unknown';
                    break;
                }


                this.cards.push({
                  name: d.name,
                  src: src,
                  ext: ext_type
                })
              }
            })
          } else {
            if (d.prefix) {
              const src = 'icon_file', ext_type = 'file';
              this.cards.push({
                name: d.prefix.slice(0, -1),
                src: src,
                ext: ext_type
              })
            }

          }

        })
      })
    }
  },
  computed: {
    computedHeaders: function () {
      console.log('this.$route.fullPath', this.$route.fullPath)
      return {
        'Authorization': sessionStorage.getItem('token'),
        'bucket': this.bucketName,
        'folder': this.$route.fullPath.split('/').slice(3).join('/')
      }
    }
  }
})
</script>

<style lang="scss">
@import "../index.scss";
.page-header {
  margin: 1rem;

  .header-info {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  .header-button {
    display: flex;
    align-items: center;
    justify-content: right;

    .el-button.upload {
      background-color: $color-primary;
    }

    .el-button.upload:hover {
      background-color: lighten($color: $color-primary, $amount: 10%);
    }
  }
}

.page {
  margin: 1rem;
  height: 90vh;

  .el-row {
    height: calc(100% - 6rem);
    overflow-y: scroll;
  }

  .el-pagination {
    margin: 1rem 0;
  }
}
</style>

Login.vue

进行基础的登录/注册实现,可在外侧进行弹窗及嵌入的包裹,将业务逻辑与展现形式分离

<template>
  <div :class="loginClass">
    <section class="login-header">
      <span class="title">{{ title }}</span>
    </section>
    <section class="login-form">
      <template v-if="form == 'login'">
        <el-form
          ref="login-form"
          label-width="70px"
          label-position="left"
          :model="loginForm"
          :rules="loginRules"
        >
          <el-form-item
            :key="item.prop"
            v-for="item in loginFormItems"
            :label="item.label"
            :prop="item.prop"
          >
            <el-input
              v-model="loginForm[`${item.prop}`]"
              :placeholder="item.placeholder"
              :type="item.type"
            ></el-input>
          </el-form-item>
        </el-form>
      </template>
      <template v-else-if="form == 'register'">
        <el-form
          ref="register-form"
          label-width="100px"
          label-position="left"
          :model="registerForm"
          :rules="registerRules"
        >
          <el-form-item
            :key="item.prop"
            v-for="item in registerFormItems"
            :label="item.label"
            :prop="item.prop"
          >
            <el-input
              v-model="registerForm[`${item.prop}`]"
              :placeholder="item.placeholder"
              :type="item.type"
            ></el-input>
          </el-form-item>
        </el-form>
      </template>
    </section>
    <section class="login-select">
      <span class="change" v-if="form == 'login'" @click="isShow = true">修改密码</span>
      <span class="go" @click="handleGo(form)">{{ form == 'login' ? ' 去注册 >>' : ' 去登录 >>' }}</span>
    </section>
    <section class="login-button">
      <template v-if="form == 'login'">
        <el-button @click="handleLogin">登录</el-button>
      </template>
      <template v-else-if="form == 'register'">
        <el-button @click="handleRegister">注册</el-button>
      </template>
    </section>
  </div>
  <el-dialog v-model="isShow">
    <el-form
      ref="change-form"
      label-width="130px"
      label-position="left"
      :model="changeForm"
      :rules="changeRules"
    >
      <el-form-item
        :key="item.prop"
        v-for="item in changeFormItems"
        :label="item.label"
        :prop="item.prop"
      >
        <el-input
          v-model="changeForm[`${item.prop}`]"
          :placeholder="item.placeholder"
          :type="item.type"
        ></el-input>
      </el-form-item>
    </el-form>
    <div class="change-button">
      <el-button class="cancel" @click="isShow = false">取消</el-button>
      <el-button class="confirm" @click="handleConfirm" type="primary">确认</el-button>
    </div>
  </el-dialog>
</template>

<script lang="ts">
import {
  defineComponent
} from 'vue';

import { validatePwd, validateEmail, validateName, validatePhone } from '../utils/index';


export default defineComponent({
  name: 'Login',
  props: {
    title: {
      type: String,
      default: ''
    },
    border: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      form: 'login',
      isShow: false,
      loginForm: {
        phone: '',
        upwd: ''
      },
      loginRules: {
        phone: [
          {
            required: true,
            validator: validatePhone,
            trigger: 'blur',
          }
        ],
        upwd: [
          {
            validator: validatePwd,
            required: true,
            trigger: 'blur',
          }
        ]
      },
      loginFormItems: [
        {
          label: "手机号",
          prop: "phone",
          placeholder: '请输入手机号'
        },
        {
          label: "密码",
          prop: "upwd",
          placeholder: '',
          type: 'password'
        }
      ],
      registerForm: {
        name: '',
        tfs: '',
        email: '',
        phone: '',
        upwd: '',
        rpwd: ''
      },
      registerFormItems: [
        {
          label: "姓名",
          prop: "name",
          placeholder: ''
        },
        {
          label: "TFS账号",
          prop: "tfs",
          placeholder: ''
        },
        {
          label: "邮箱",
          prop: "email",
          placeholder: ''
        },
        {
          label: "手机号",
          prop: "phone",
          placeholder: ''
        },
        {
          label: "请输入密码",
          prop: "upwd",
          placeholder: '',
          type: 'password'
        },
        {
          label: "请确认密码",
          prop: "rpwd",
          placeholder: '',
          type: 'password'
        }
      ],
      registerRules: {
        name: [
          {
            validator: validateName,
            trigger: 'blur',
          }
        ],
        tfs: [
          {
            required: true,
            message: '请按要求输入tfs账号',
            trigger: 'blur',
          }
        ],
        email: [
          {
            required: true,
            validator: validateEmail,
            trigger: 'blur',
          }
        ],
        phone: [
          {
            required: true,
            validator: validatePhone,
            trigger: 'blur',
          }
        ],
        upwd: [
          {
            required: true,
            validator: validatePwd,
            trigger: 'blur',
          }
        ],
        rpwd: [
          {
            required: true,
            validator: validatePwd,
            trigger: 'blur',
          },
          {
            validator(rule: any, value: any, callback: any) {
              if (value != this.registerForm.upwd) {
                callback(new Error('输入的密码不同'))
              }
            },
            trigger: 'blur',
          }
        ],
      },
      changeForm: {
        phone: '',
        opwd: '',
        npwd: '',
        rpwd: ''
      },
      changeFormItems: [
        {
          label: "手机号",
          prop: "phone",
          placeholder: '请输入手机号'
        },
        {
          label: "请输入原始密码",
          prop: "opwd",
          placeholder: '',
          type: 'password'
        },
        {
          label: "请输入新密码",
          prop: "npwd",
          placeholder: '',
          type: 'password'
        },
        {
          label: "请重复新密码",
          prop: "rpwd",
          placeholder: '',
          type: 'password'
        }
      ],
      changeRules: {
        phone: [
          {
            required: true,
            validator: validatePhone,
            trigger: 'blur',
          }
        ],
        opwd: [
          {
            required: true,
            validator: validatePwd,
            trigger: 'blur',
          }
        ],
        npwd: [
          {
            required: true,
            validator: validatePwd,
            trigger: 'blur',
          }
        ],
        rpwd: [
          {
            required: true,
            validator: validatePwd,
            trigger: 'blur',
          },
          {
            validator(rule: any, value: any, callback: any) {
              if (value != this.changeForm.npwd) {
                callback(new Error('输入的密码不同'))
              }
            },
            trigger: 'blur',
          }
        ],
      }
    }
  },
  computed: {
    loginClass() {
      return this.border ? 'login login-unwrapper' : 'login login-wrapper'
    }
  },
  methods: {
    handleGo(form) {
      if (form == 'login') {
        this.form = 'register'
      } else if (form == 'register') {
        this.form = 'login'
      }
    },
    handleLogin() {
      this.$http.post("/bff/imagepic/auth/login", {
        phone: this.loginForm.phone,
        upwd: this.loginForm.upwd
      }).then(res => {
        if (res.data.success) {
          this.$message.success('登录成功');
          sessionStorage.setItem('token', res.data.data.token);
          this.$router.go(0);
        } else {
          this.$message.error(res.data.data.err);
        }
      })
    },
    handleRegister() {
      this.$http.post("/bff/imagepic/auth/register", {
        name: this.registerForm.name,
        tfs: this.registerForm.tfs,
        email: this.registerForm.email,
        phone: this.registerForm.phone,
        upwd: this.registerForm.upwd
      }).then(res => {
        if (res.data.success) {
          this.$message.success('注册成功');
        } else {
          this.$message.error(res.data.data.err);
        }
      })
    },
    handleConfirm() {
      this.$http.post("/bff/imagepic/auth/change", {
        phone: this.changeForm.phone,
        opwd: this.changeForm.opwd,
        npwd: this.changeForm.npwd
      }).then(res => {
        if (res.data.success) {
          this.$message.success('修改密码成功');
        } else {
          this.$message.error(res.data.data.err);
        }
      })

    }
  }

})
</script>

<style lang="scss">
@import "../index.scss";
.login-wrapper {
}

.login-unwrapper {
  border: 1px solid #ececec;
  border-radius: 4px;
}

.login {
  &-header {
    text-align: center;
    .title {
      font-size: 1.875rem;
      font-size: bold;
      color: #333;
    }
  }

  &-form {
    margin-top: 2rem;
  }

  &-select {
    display: flex;
    justify-content: right;
    align-items: center;
    cursor: pointer;

    .go {
      color: orange;
      text-decoration: underline;
      margin-left: 0.5rem;
    }

    .go:hover {
      color: orangered;
    }

    .change {
      color: skyblue;
    }

    .change:hover {
      color: rgb(135, 178, 235);
    }
  }

  &-button {
    margin-top: 2rem;
    .el-button {
      width: 100%;
      background-color: $color-primary;
      color: white;
    }
  }
}

.change-button {
  display: flex;
  justify-content: space-around;
  align-items: center;

  .confirm {
    background-color: $color-primary;
  }
}
</style>

routes.ts

vue-router@next中的动态路由方案略有不同,有类似rank的排名机制,具体可以参考vue-router@next的官方文档

import { WrapperLayouts } from '../components';
import menuMap from './menuMap'
// 1. 定义路由组件, 注意,这里一定要使用 文件的全名(包含文件后缀名)
const routes = [
    { 
        path: "/",
        component: WrapperLayouts,
        redirect: `/page/${Object.keys(menuMap)[0]}`,
        children: [
            {
                path: '/page/:id',
                name: 'page',
                component: () => import('../views/Page.vue'),
                children: [
                {
                    path: '/page/:id(.*)*',
                    // redirect: `/page/${Object.keys(menuMap)[0]}`,
                    name: 'pageno',
                    component: () => import('../views/Page.vue')
                }
                ]
            }
        ]
    },
];

export default routes;
import {createRouter, createWebHashHistory} from 'vue-router';

import { routes } from '../config';


// Vue-router新版本中,需要使用createRouter来创建路由
export default  createRouter({
  // 指定路由的模式,此处使用的是hash模式
  history: createWebHashHistory(),
  routes // short for `routes: routes`
})

Aside.vue

结合路由进行左边侧边栏的路由跳转及显示

<template>
  <div class="aside">
    <el-menu @select="handleSelect" :default-active="Array.isArray($route.params.id) ? $route.params.id[0] : $route.params.id">
      <el-menu-item v-for="(menu, index) in menuLists" :index="menu.id" >
        <span>{{menu.label}}</span>
      </el-menu-item>
    </el-menu>
  </div>
</template>

<script lang="ts">
import {
  computed,
  defineComponent,
  getCurrentInstance,
  onMounted,
  reactive,
  ref,
  toRefs,
} from 'vue';

export default defineComponent({
  name: 'Aside',
  props: {
    menuMap: {
      type: Object,
      default: () => {}
    }
  },
  components: {

  },
  methods: {
    handleSelect(e) {
      console.log('$route', this.$route.params.id)
      console.log('select', e)
      this.$router.push(`/page/${e}`)
    }
  },
  setup(props, context) {
    console.log('props', props.menuMap)
    //引用全局变量
    const { proxy } = getCurrentInstance();

    const menuMap = props.menuMap;

    let menuLists = reactive([]);

    //dom挂载后
    onMounted(() => {
      handleMenuLists();
    });

    function handleMenuLists() {
      (proxy as any).$http.get('/bff/imagepic/bucket/listBuckets').then(res => {
        console.log('listBuckets', res);
        if(res.data.success) {
          res.data.data.forEach(element => {
            menuMap[`${element.name}`] && menuLists.push({
              id: element.name,
              label: menuMap[`${element.name}`]
            }) 
          })
        }
      })
    }


    return {
      ...toRefs(menuLists),
      handleMenuLists,
      menuLists
    };
  }
})
</script>

<style lang="scss">
.aside {
  height: 100%;
  background-color: #fff;
  width: 100%;
  border-right: 1px solid #d7d7d7;
}
</style>

总结

前端图床作为前端基建侧的一项重要的开发工具,不仅能够为业务开发人员提供更好的开发体验,也能节省业务开发过程中造成的效率降低,从而提升开发效率,降低成本损耗。前端展示的实现有多种不同的方案,对于有着更高要求的前端图床实现也可以基于需求进行更高层次的展示与提升。

相关文章
|
11天前
|
前端开发 JavaScript 开发者
优化前端性能的关键技巧与实践
在当今互联网时代,前端性能优化是网站和应用开发中至关重要的一环。本文将介绍一些关键的前端性能优化技巧,并提供实际的实践案例,帮助开发者有效提升网站和应用的性能表现。
165 53
|
5天前
|
缓存 前端开发 JavaScript
微前端框架开发实践的体验报告
微前端架构作为一种解决方案,通过将应用拆分成更小、更易于管理的子应用来提高开发效率和应用性能。本文将分享我在开发微前端框架过程中遇到的问题、解决思路以及具体方案。通过本次微前端框架的开发实践,我们成功实现了应用的解耦和性能的提升。关键点包括跨域问题的解决、路由分发的实现、沙箱和样式隔离的技术应用、通信机制的构建以及性能优化策略的采用。我们的成果是建立了一个高效、可扩展、易于维护的微前端架构。同时,我们也认识到了微前端架构的复杂性,以及在实施过程中需要考虑的诸多细节问题。
46 0
|
9天前
|
前端开发 Java Go
从前端到后端:构建现代化Web应用的技术实践
本文将介绍如何通过前端和后端技术相结合,构建现代化Web应用的技术实践。我们将探讨前端开发、后端架构以及多种编程语言(如Java、Python、C、PHP、Go)在构建高效、可扩展的Web应用中的应用。
|
10天前
|
缓存 算法 前端开发
前端开发者必知的缓存淘汰策略:LRU算法解析与实践
前端开发者必知的缓存淘汰策略:LRU算法解析与实践
|
10天前
|
Web App开发 前端开发 JavaScript
构建跨浏览器兼容的前端应用:技术实践与挑战
【5月更文挑战第16天】构建跨浏览器兼容的前端应用是应对浏览器差异和多样性的挑战。使用现代框架(如React、Vue)能自动转换代码,编写可移植的Web标准代码,结合浏览器兼容性测试工具和Polyfill解决旧浏览器支持问题。关注浏览器更新,应对性能、API差异和样式问题,采用渐进增强、条件判断和CSS Reset策略确保应用在各种浏览器上运行良好。
|
11天前
|
缓存 监控 前端开发
使用Angular进行企业级前端开发:实践与探索
【5月更文挑战第14天】本文探讨了使用Angular进行企业级前端开发的实践与技术要点。Angular,Google的开源JavaScript框架,以其组件化设计、响应式开发和依赖注入等特点成为首选。文章涵盖项目初始化、组件化、状态管理、路由、性能优化和测试部署等方面,强调了在企业级应用中如何利用Angular构建高效、稳定和可维护的前端解决方案。
|
11天前
|
前端开发 JavaScript API
深入理解前端开发:从基础到实践
深入理解前端开发:从基础到实践
|
11天前
|
移动开发 前端开发 UED
【专栏:HTML与CSS前端技术趋势篇】渐进式增强与优雅降级在前端开发中的实践
【4月更文挑战第30天】前端开发中的渐进式增强和优雅降级是确保跨浏览器、跨设备良好用户体验的关键策略。渐进式增强是从基础功能开始,逐步增加高级特性,保证所有用户能访问基本内容;而优雅降级则是从完整版本出发,向下兼容,确保低版本浏览器仍能使用基本功能。实践中,遵循HTML5/CSS3规范,使用流式布局和响应式设计,检测浏览器特性,并提供备选方案,都是实现这两种策略的有效方法。选择合适策略优化网站,提升用户体验。
|
11天前
|
Dart 前端开发 测试技术
【Flutter前端技术开发专栏】Flutter开发中的代码质量与重构实践
【4月更文挑战第30天】随着Flutter在跨平台开发的普及,保证代码质量成为开发者关注的重点。优质代码能确保应用性能与稳定性,提高开发效率。关键策略包括遵循最佳实践,编写可读性强的代码,实施代码审查和自动化测试。重构实践在项目扩展时尤为重要,适时重构能优化结构,降低维护成本。开发者应重视代码质量和重构,以促进项目成功。
【Flutter前端技术开发专栏】Flutter开发中的代码质量与重构实践
|
11天前
|
Dart 前端开发 安全
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
【4月更文挑战第30天】本文探讨了Flutter中线程管理和并发编程的关键性,强调其对应用性能和用户体验的影响。Dart语言提供了`async`、`await`、`Stream`和`Future`等原生异步支持。Flutter采用事件驱动的单线程模型,通过`Isolate`实现线程隔离。实践中,可利用`async/await`、`StreamBuilder`和`Isolate`处理异步任务,同时注意线程安全和性能调优。参考文献包括Dart异步编程、Flutter线程模型和DevTools文档。
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践