vueImitated Millet mall – small workshop actual combat record

This is a vue bucket project imitating Xiaomi Mall, click preview

Project environment introduction:

  • System:macos
  • Package management tools:yarn
  • Node: v12.4.0

Pages and features the project will complete:

  • Login page -> Encapsulate form validation method
  • Home -> Achieve forward and backward routing animation
  • Category page -> Use third party lazy load components
  • Details -> Encapsulationpopupcomponent
  • Shopping cart – >vueA list of animation

Some animations should be added to the project to enrich the interaction

General knowledge involved in the project:

  • vue 3.xLatest Use of scaffolding
  • webstormUse tricks
  • webpackConfiguration optimization
  • vueGeneric component encapsulation
  • vwMobile terminal adaptation and tricking practice
  • jsDOCTo write comments for utility functions
  • mockjsPerform data simulation
  • Package and deploy togithub page

    . And so on

In the process of writing code, I will pay attention to my code specification and the readability of naming, and I will also learn and record in this process. Let’s start this exciting journey together.

Quick start

We can quickly get the project up and running, packaged and published with the following commands:

Git clone [email protected]: wangkaiwd/xiaomi - shop. Git CD xiaomi - shop # start projects yarn start # package yarn yarn build # analysis package files Build: Analysis # Deploy to github Page YARN deployCopy the code

The directory structure of the project is as follows:

Mime-shop ├─.Browserslistrc ├─.env.analysis // Vue CLI Environment Variables File ├─.gitignore ├─ Readme.md ├─ Babel.config.js ├─ The deploy. Sh / / project deployment script ├ ─ package. The json ├ ─ postcss. Config. Js ├ ─ public │ ├ ─ the favicon. Ico │ ├ ─ img │ │ └ ─ the ICONS │ ├ ─ Index.html │ ├ ─ the manifest. Json │ └ ─ robots. TXT ├ ─ screenshots / / project screenshots │ ├ ─ calc - SCSS. PNG │ ├ ─ icon - the font - link. PNG │ └ ─ Icon - the font - prefix. PNG ├ ─ SRC │ ├ ─ MiApp. Vue │ ├ ─ API / / interface API │ │ └ ─ index. The js │ ├ ─ assets/resources/static │ │ ├ ─ img │ │ └ ─ Styles │ ├─ Bass Exercises - Dialog │ ├─ footerNav │ ├─ Bass Exercises - Icon │ ├─ Layout │ ├─ number │ │ ├ ─ popup │ │ ├ ─ skeleton │ │ ├ ─ toast │ │ └ ─ topHeader │ ├ ─ config/configuration/projects │ │ └ ─ navConfig. Js │ ├ ─ helpers / / help function │ │ ├─ Anti-Flag. Js │ │ ├─ Anti-Flag. Js │ │ ├─ Anti-Flag │ │ └ ─ the validator. Js │ ├ ─ HTTP / / axios related encapsulation │ │ ├ ─ axiosConfig. Js │ │ └ ─ request. Js │ ├ ─ main. Js / / entry documents │ ├ ─ RegisterServiceWorker. Js │ ├ ─ the router / / routing configuration │ │ ├ ─ lazyLoading. Js │ │ └ ─ the router. The js │ ├ ─ store / / vuex │ │ └ ─ store. Js │ └ ─ views / / project page │ ├ ─ category │ ├ ─ the detail │ ├ ─ example │ ├ ─ home │ ├ ─ homeCategory │ ├ ─ login │ ├ ─ mime │ ├ ─ search │ └─ shopCart ├─ vue.config.js // webpack setup ├─ damn.lockCopy the code

Project creation

Here we use the vue CLI provided by VUE to initialize the project:

yarn global add @vue/cli
vue create xiaomi-shop
Copy the code

If we find that we have already installed the Vue CLI, to ensure that the CLI tool is the latest version, we can upgrade the version:

yarn global upgrade @vue/cli
Copy the code

After that, you can choose the required modules and tools according to the PROMPTS of cli tools for development. The author uses the following options: Babel+Router(mode: Hash)+Vuex+Sass/SCSS(with Dart-sass)

Dart-sass is used here because Node-sass always has problems during the download and installation process

configurationwebpack

Next we configure webpack in vue.config.js, my configuration code is here: portal

The configuration file does the following things:

  1. Shut downeslint
  2. Set global variables to facilitate the packaging of different environments
  3. Configuring a Path Alias
  4. Configuration file extension
  5. Automatic global importcss
  6. Set up thefaviconIcon path
  7. Remove the packagedconsole.log
  8. throughHardSourceWebpackPluginCache packaging intermediate steps to improve performance
  9. opengzip
  10. useautodll-webpack-pluginPackage third-party modules and files that are not frequently changed in advance to improve the packaging speed

There is also a community summary of a detailed configuration file for vue.config.js: Portal

The HardSourceWebpackPlugin and autodll-webpack-plugin are highlighted here. After using these two plug-ins in the project, the first package speed didn’t increase much, but the second package saved nearly 80% of the package time. Try this if you’re having trouble packing (it’s easy to configure in the React project).

Add the corresponding shortcut to package.json when you are done:

"scripts": {
  "start": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "build:analysis": "vue-cli-service build --mode analysis",
  "deploy": "sh ./deploy.sh"
},
Copy the code

webstormPractical skills

We can provide WebStorm with the WebPack configuration file, so that WebStorm can recognize the configuration of path aliases and suffixes, which greatly facilitates WebStorm to automatically import our path completion and code.

Vue’s webpack.config.js is here, and it dynamically recognizes the configuration in vue. Config.js:

If we’re building our project using react-create-app and don’t want to use eject, we can write a fake webpack.config.js file specifically for WebStorm to recognize:

// This is not a real WebPack configuration file, but is used to make WebPack recognize the corresponding configuration
const path = require('path');
module.exports = {
  resolve: {
    alias: {
      The '@': path.resolve(__dirname, './src')}}};Copy the code

In the project we disabled the ESLint plugin and used WebStorm to control our code style. Once configured, we just formatted it:

Here our JavaScript code partitions follow the default standard code style and are set to end each line with a semicolon

In code style, you can also set the code style of CSS, HTML, SASS and other files. You can study it yourself.

Here are a few more shortcuts that I find particularly useful:

I use a MAC

  • shift+F6: You can rename variables and change where variables are used, which greatly facilitates code refactoring
  • ctrl+B: When not using the mouse, you can jump to a function or variable definition using the keyboard
  • option+enter: pops up a code prompt window, especially useful when importing dependencies automatically
  • ctrl+[ / ctrl+]: can jump to the point before or after we operate the code to make it passctrl+BJumping to the definition and back again is surprisingly quick

Install third-party project dependencies

We also used some of the best third-party plugins in the community:

  • vue-awesome-swiper: vueVersion of theswiperPlugins, all supportedswiperIn theapi
  • vue-lazyload: vueImage lazy loading plugin
  • axios: support toPromiseIs sent in the form ofhttprequest
  • Nprogress: implements the header loading progress bar
  • VConsole: mobile page development tool

VConsole is only used in the development environment:

if (process.env.NODE_ENV === 'development') {
  const VConsole = require('vconsole');
  const vConsole = new VConsole();
}
Copy the code

There has always been a saying in programming: Don’t duplicate the wheel. Especially in the work, the development pays more attention to efficiency, the use of some excellent third-party plug-ins and third-party component library can better assist our work, we should carry out secondary packaging on the original components to improve the development efficiency.

But when it comes to learning, we can still improve our personal strength by lifting the wheels. Just because we don’t object to repeating the wheel doesn’t mean we don’t have the ability to do it.

Adaptation scheme

The project uses VW unit for mobile end adaptation to be compatible with different models.

First we need to install the following dependencies:

yarn add cssnano cssnano-preset-advanced postcss-aspect-ratio-mini postcss-cssnext postcss-import postcss-px-to-viewport  postcss-url postcss-viewport-units postcss-write-svg -DCopy the code

Then add the following configuration to postcss.config.js:

module.exports = {
  plugins: {
    'postcss-import': {},
    'postcss-url': {},
    'postcss-aspect-ratio-mini': {},
    'postcss-write-svg': {
      'utf8': false
    },
    'postcss-cssnext': {},
    // document address: https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md
    'postcss-px-to-viewport': {
      'viewportWidth': 375.'unitPrecision': 5.'selectorBlackList': [
        '.ignore'.'.hairlines'].'mediaQuery': false
    },
    'postcss-viewport-units': {
      / / filter when using pseudo elements covered plug-in generated content in the command line of warning:https://github.com/didi/cube-ui/issues/296
      filterRule: rule= > rule.nodes.findIndex(i= > i.prop === 'content') = = =- 1
    },
    'cssnano': {
      'preset': 'advanced'.'autoprefixer': false.'postcss-zindex': false}}};Copy the code

The important thing to note here is that the viewport configuration item, which we set to 375, in the real world the UI designer will give us a double graph, which is 750. If you want a configuration item, you can refer to the documentation: Portal

Guidelines on pit

In the process of using VW adaptation scheme, the following two problems were probably encountered:

  • Add using pseudo-elementscontentProperty is prompted on the command lineerror
  • Set up thestyleUnable to convert tovw

}}}}}}}}}}}}}}}}}}}}}}}}}}}

'postcss-viewport-units': {
  / / filter when using pseudo elements covered plug-in generated content in the command line of warning:https://github.com/didi/cube-ui/issues/296
  filterRule: rule= > rule.nodes.findIndex(i= > i.prop === 'content') = = =- 1
}
Copy the code

The problem with style conversion vw is that we simply wrote a js method to help us with the conversion:

export const vw = (number) = > {
  const htmlWidth = document.documentElement.offsetWidth;
  return number * (100 / htmlWidth);
};
Copy the code

In this way, we simply solved some minor problems encountered in the current development.

Universal component design

For common components, since they are introduced in many places globally, we use webpack’s require.context method to automatically register the global component for ease of use. I put it in a separate JS file to execute:

// autoRegister.js
import Vue from 'vue';
// Components that do not require automatic registration
const blackList = ['MuiToast'];
const requireComponent = require.context('components'.true, /Mui[A-Z]\w+\.vue$/);
requireComponent.keys().forEach(filename= > {
  const componentConfig = requireComponent(filename);
  const start = filename.lastIndexOf('/') + 1;
  const end = filename.lastIndexOf('. ');
  const componentName = filename.slice(start, end);
  if (blackList.includes(filename)) {return; }// Globally register components
  Vue.component(
    componentName,
    // If the component option is exported via 'export default',
    // Then '.default 'is preferred,
    // Otherwise fall back to the root of the use module.
    componentConfig.default || componentConfig
  );
});
Copy the code

Of course, we need to define a naming convention here: component names must start with Mui and follow the rules for camel naming

Based on project requirements, I implemented the following common components:

  • layoutLayout components (MuiLayout,MuiHeder,MuiFooter,MuiAside,MuiContent)
  • iconFont Icon Component (MuiIcon)
  • popupPop-up component (MuiPopup)
  • dialogDialog box component (MuiDialog)
  • toastGlobal prompt (MuiToast)
  • numberProduct Add button (MuiNumber)

Here we mainly talk about the implementation process of icon and Toast components. You can see the source code for the implementation process of other components.

iconcomponent

Icon icon is used very frequently in the project, so IT is necessary for me to create a unified package for easy use.

The icon icon used in the project was obtained from the iconfont website: portal. Here we use symbol to implement, can support multi-color ICONS, can also use font size, color to adjust the style.

First of all, we need to select our icon in the icon library, and then we can make simple Settings for the project of our icon:

We then select the icon of type Symbol and copy the address to pubic/index.html.


      
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport"
        content="Width =device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <title>Millet mall</title>
  <script src="//at.alicdn.com/t/font_1253950_whicd7mh5w.js"></script>
</head>
<body>
<noscript>
  <strong>We're sorry but vue-cli-demo doesn't work properly without JavaScript enabled. Please enable it to
    continue.</strong>
</noscript>
<div id="app"></div>
<! -- built files will be auto injected -->
</body>
</html>
Copy the code

After the preparation, we created the MuiIcon file and added the following code:

<template>
  <svg
    class="mui-icon"
    aria-hidden="true"
  >
    <use xlink:href="#icon-xxx"></use>
  </svg>
</template>

<script>
  export default {
    name: 'MiIcon',
  };
</script>

<style lang="scss" scoped>
  .mui-icon {
    display: inline-block;
    width: 1em; height: 1em;
    vertical-align: top;
    fill: currentColor;
    overflow: hidden;
  }
</style>
Copy the code

CSS is not covered here

The XXX in the code needs to be replaced with the name of the corresponding icon during use. We dynamically set the icon name by passing a name attribute to the icon component. Since the project icon is prefixed with mi, the following changes will be made:

<template>
  <svg
    class="mui-icon"
    aria-hidden="true"
  >
    <use :xlink:href="`#mi-${name}`"></use>
  </svg>
</template>

<script>
  export default {
    name: 'MiIcon',
    props: {
      name: { type: String, required: true }
    }
  };
</script>
Copy the code

Thus we have implemented a basic Icon component that can be used in projects like this:

<mui-icon name="logo"></mui-icon>
Copy the code

In our daily projects, we also encounter the following requirements:

  • The mouse movingiconIcon, icon rotation
  • Click on theiconJump to a page

It is not possible to add properties and methods to the Icon component one by one. Here we use some of the less commonly used apis in Vue:

  • v-onandv-bindBound object: The properties of the object are distributed to the current node
  • $attrs: can get not inpropsAttributes defined in
  • $listens: Gets none in the parent scope.nativeThe decoratorv-onEvent listener
  • inheritAttrs: Can let notpropsProperties added to theiconOn the root node of the component
<template> <svg class="mui-icon" aria-hidden="true" v-bind="$attrs" v-on="$listeners" > <use :xlink:href="`#mi-${name}`"></use> </svg> </template> <script> export default { name: 'MiIcon', inheritAttrs: False, // The default value is true, whether to display an attribute passed to the root node that was not received by props: {name: {type: String, required: true}}; </script>Copy the code

Once written in this way, the Icon component can accept any of the events and properties that SVG natively supports.

React will encounter similar requirements, and react will not help us merge classes. So the idea in React is as follows:

  • The individualclassManually concatenate multiple class name format (VueThis is done for us.)
  • through. restPropsExtend the remaining attributes to the corresponding node

toastcomponent

Toast is not used in the same way as the other components. It is registered globally using vue.use. When we use the Vue. Use method, we pass in an install method that takes the Vue instance and configuration options as arguments.

export default {
  install (Vue,options) {
    
  }
};
Copy the code

A quick glance at the source code shows that the above install method is also executed when vue. use is executed.

In the VUE community, we often see examples of calling components directly from functions on vUE instances:

this.$toast('This is a toast');
this.$toast({ message: 'Loading... '.type: 'loading'.mask: true })
Copy the code

This way of calling is because we bind the corresponding method to the vue prototype, which can then be accessed directly on the vue instance object. Combining what we said above, the code looks something like this:

export default {
  install (Vue) {
    Vue.prototype.$toast = (options) = > {
      // doSomeThing}; }};Copy the code

This way we can add the $toast method to the Vue prototype via vue. use, which is easy to call directly from the component.

At this point, we have roughly determined how our component will be called, and we have designed the parameters as follows:

  • message: Prompt message
  • mask: Whether there is a mask layer
  • type: Prompt type, when passed inloading, the loading status can be displayed
  • icon: Prompts font icon display
  • duration: Displays the event in milliseconds. The event is not automatically closed when 0 is passed in

Post my implementation code (excluding CSS):

<template> <transition name="fade"> <div class="mui-toast" v-if="visible"> <div class="mui-toast-content" :class="{hasIcon}"> <div class="mui-toast-icon" v-if="hasIcon"> <mui-icon class="mui-toast-icon-loading" v-if="isLoading" name="loading"></mui-icon> <mui-icon v-else :name="icon"></mui-icon> </div> {{message}} </div> <div class="mui-toast-mask" v-if="mask"></div> </div> </transition> </template> <script> export default { name: 'MuiToast', props: { message: { type: String, }, mask: { type: Boolean, default: false }, type: { type: String, validator (value) { return ['default', 'loading'].includes(value); }, default: 'default' }, icon: { type: String }, duration: { type: Number, default: 3000 } }, data () { return { visible: false }; }, computed: { isLoading () { return this.type === 'loading'; }, hasIcon () { return this.isLoading || this.icon; } }, mounted () { this.visible = true; this.autoClose(); }, methods: { closeToast () { this.visible = false; this.$nextTick(() => { this.$el.remove(); this.$destroy(); }); }, autoClose () { if (this.duration === 0 || this.type === 'loading') {return; } setTimeout(() => { this.closeToast(); }, this.duration); }}}; </script>Copy the code

The idea is to define visible: False in data first, and then set Visible: True after the component is mounted, so that the transition component can animate the component when it appears and is destroyed.

Note that if we animate children of the root element, we need to specify the transition time explicitly, otherwise the animation will not work

The document address

After the component is created, instead of calling it directly, we use some OF vue’s apis to dynamically generate the component and render the content into the body:

export default {
  install (Vue) {
    Vue.prototype.$toast = (options) = > {
      // Pass the Toast component configuration item for 'vue.extend' to generate the constructor
      const componentClass = Vue.extend(Toast);
      // Create 'toastInstance' dynamically through the constructor
      const toastInstance = new componentClass({
        // Pass parameters via propsData
        propsData: options,
      });
      // If no render node is specified for $mount, the component can be inserted into the document via the native DOM API
      toastInstance.$mount();
      document.body.appendChild(toastInstance.$el); }; }};Copy the code

For dynamically creating vUE components and rendering them into a page, see this article:

At this point, a basic Toast component is roughly complete

After the test, I probably found the following problems:

  • Click repeatedly to create the component
  • Unable to close the component outside the component, causingloadingCan’t shut down
  • Provides simplified invocation methods:this.$toast(message), without passing in complex configuration items, easy to use

Here we receive the generated component instance through an external variable and remove the old instance and DOM structure from the page with each creation. After creating a component from a function, it returns a function to close the component, which we can call directly:

import Toast from './MuiToast';
let toastInstance = null;
export default {
  install (Vue) {
    Vue.prototype.$toast = (options) = > {
      // If the component already exists, destroy it and recreate it
      if (toastInstance) { // You can call the methods in the component directly from the instance
        toastInstance.closeToast();
      }
      const componentClass = Vue.extend(Toast);
      if (typeof options === 'string') {
        options = { message: options };
      }
      toastInstance = new componentClass({
        propsData: options,
      });
      toastInstance.$mount();
      document.body.appendChild(toastInstance.$el);
      // Returns the close function after the component is called
      returntoastInstance.closeToast; }; }};Copy the code

The effects used in the project are as follows:

Knowledge anecdotal stories

In the writing process of the project, I gained more knowledge about the use of import and export in ES6.

Here is a question to test friends, interested please leave a comment below.

Create 3 new files in the project SRC directory: a.js,b.js and c.js, where A.js is the entry file (that is, the first one to execute). The code in each file is as follows:

// a.js
console.log('a.js');
import './b.js'

// b.js
console.log('b.js');
import './c.js'

// c.js
console.log('c.js');
import './a.js'
Copy the code

What is the final output? Anyway, this is a subversion of the author’s perception

Resources: Load implementation of Module

conclusion

It took about two months to write and summarize this project. The author shared what he saw and learned, hoping it would be helpful to everyone.

Open source is not easy, we hope you can give a start to encourage developers in the community willing to share to create better games.

Source code address: Xiaomi -shop

Another vUE project: VUE + Element background management system. When VUE is combined with Element UI, a different spark will be generated.