webpack+vue搭建todo應用

webpack+vue搭建todo應用

使用方法:

下載安裝javascript

git clone https://github.com/mandyshen97/vue-todo.git

運行css

npm run dev


  • 目的:html

    • 搭建前端工程化項目webpack+vue
  • 目標:前端

    • 配置開發時前端工程
    • 實現一個簡單的TODO應用
    • 優化配置達到上線標準
  • 環境版本:
"vue": "^2.6.10",
"vue-loader": "^15.7.0",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.32.2",
"webpack-dev-server": "^3.5.1",
"webpack-cli": "^3.3.2"
  • 依賴:
"dependencies": {
    "autoprefixer": "^9.5.1",
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5",
    "babel-plugin-syntax-jsx": "^6.18.0",
    "babel-plugin-transform-vue-jsx": "^3.7.0",
    "babel-preset-env": "^1.7.0",
    "cross-env": "^5.2.0",
    "css-loader": "^2.1.1",
    "extract-text-webpack-plugin": "^4.0.0-beta.0",
    "file-loader": "^3.0.1",
    "html-webpack-plugin": "^3.2.0",
    "postcss-loader": "^3.0.0",
    "style-loader": "^0.23.1",
    "stylus": "^0.54.5",
    "stylus-loader": "^3.0.2",
    "url-loader": "^1.1.2",
    "vue": "^2.6.10",
    "vue-loader": "^15.7.0",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.32.2",
    "webpack-dev-server": "^3.5.1"
  },
  "devDependencies": {
    "webpack-cli": "^3.3.2"
  }

vue+webpack項目工程配置

建立webpack項目

項目初始化

  • 進入項目文件夾,在終端
npm init

生成 package.jsonvue

  • 安裝包和依賴
npm install webpack vue vue-loader
  • 安裝後會出現一些提醒,根據提醒把須要的依賴裝上去
npm i css-loader vue-template-compiler

此時項目就初始化好了。java

webpack項目配置

編寫簡單組件

  • 創建文件夾 src ,文件夾src/assets,文件src/App.vue,入口文件src/index.js

App.vue中寫以下簡單組件代碼:node

// App.vue

<template>
  <div id="test">{{text}}</div>
</template>

<script>
export default {
  data() {
    return {
      text: 'abcd'
    }
  }
}
</script>

<style>
  #test {
    color: red;
  }
</style>

顯然這個組件是沒法在瀏覽器中直接運行的,下面的操做使它能夠運行。webpack

  • index.js中將App組件掛載到dom節點中。
// index.js

/**
 * 入口文件
 */
import Vue from 'vue';
import App from './App.vue';

const root = document.createElement('div');
document.body.appendChild(root);

// 建立Vue對象,將App組件掛載到root節點
new Vue({
  render: (h) => h(App)
}).$mount(root)
webpack是幫咱們打包前端資源的,咱們的前端資源有不少類型,好比說 javascript , css , images,字體等,這些都是要經過 http請求去加載的內容。
  • package.json同級位置創建webpack.config.js文件。
// webpack.config.js

// path是nodejs中的一個基本包,用來處理路徑的
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
  // 聲明入口,entry使用絕對路徑,保證不出錯誤
  entry: path.join(__dirname, 'src/index.js'),
  mode: 'production',
  // 出口
  output: {
    // 輸出打包文件名(將index.js以及其依賴的資源打包成bundle.js)
    filename: 'bundle.js',
    // 輸出路徑
    path: path.join(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /.css$/,
        loader:['css-loader']
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]
}
  • package.json中添加build:
只有在這裏調用這個 webpack,纔會調用咱們安裝在項目裏面的 webpack,若是不在這裏添加,直接在命令行裏面輸,則使用的是全局的 webpack,版本可能不同,應該在這裏添加。
// package.json

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
+   "build": "webpack --config webpack.config.js"
  },
  • 運行
npm run build

此時在項目文件夾下生成dist文件夾及dist/bundle.jsgit

一些loader配置

  • webpack.config.js中添加下列內容:
module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(gif|jpg|jpeg|png|svg)$/,
        use: [
          {
            loader:  'url-loader',
            options: {
              limit: 1024,
              name: '[name]-aaa.[ext]'
            }
          }
        ]
      }
    ]
  }

安裝上面對應的loadergithub

npm i style-loader url-loader file-loader
  • assets文件夾中建立assets/images,assets/styles
  • styles下建立test.css
body{
     color: red;
     background-image: url('../images/do.jpg');
 }
  • index.jsimport這個test.css和圖片文件
// index.js

import './assets/styles/test.css';
import './assets/images/bg.jpg';
  • 執行
npm run build

能夠看到圖片被打包到了dist文件夾下bundle.js中也有了test.css的內容.

  • webpack.config.js的module模塊的rules中添加css預處理器的規則。
{
  test: /\.styl/,
  use: [
      'style-loader',
      'css-loader',
      'stylus-loader'
    ]
}
npm i stylus-loader stylus
  • styles目錄下新建test-stylus.styl文件
// test-stylus.styl

body
  font-size 20px
  • index.js中引入test-stylus.styl
import './assets/styles/test-stylus.styl';
  • 運行
npm run build

此時項目的目錄結構以下:

配置webpack-dev-server

npm i webpack-dev-server
  • package.json中添加"dev"
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.config.js",
+   "dev": "webpack-dev-server --config webpack.config.js"
  },

修改webpack.config.js的配置來適應webpack-dev-server的開發模式。

module.exports = {
  // 編譯目標是web平臺
+ target: 'web',

由於在不一樣的平臺上設置環境變量的方式是不同的,使用cross-env來在不一樣的環境下使用一樣的腳本。

npm i cross-env

修改package.json

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
-   "build": "webpack --config webpack.config.js",
+   "build": "cross-env NODE_ENV=production webpack --config webpack.config.js",
-   "dev": "webpack-dev-server --config webpack.config.js",
+   "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.js"
  },

webpack.config.js中判斷:

// webpack.config.js
// path是nodejs中的一個基本包,用來處理路徑的
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

- module.exports = {
+ const isDev = process.env.NODE_ENV === 'development';

+ const config = {
  ...
}

+ if(isDev){
+  config.devServer = {
    // 端口
    port: 8080,
    // 主機
    host: '0.0.0.0',
    // 使webpack錯誤顯示到頁面上
    overlay: {
      error: true,
    }
  }
}
  • 添加html文件使項目在瀏覽器中能打開。
  • 添加html插件
npm i html-webpack-plugin

配置插件:

+ const HTMLPlugin = require('html-webpack-plugin');

plugins: [
+   new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: isDev ? '"development"' : '"production"'
      }
    }),
    new VueLoaderPlugin(),
+   new HTMLPlugin()
  ]

運行:

npm run dev

在瀏覽器中訪問:http://127.0.0.1:8080/
http://localhost:8080/

此時改變頁面內容,保存,瀏覽器的顯示能夠自動刷新!!

  • 添加熱替換和devtool
// 若是是開發模式,則進行下列配置
if(isDev){
  // 控制是否生成,以及如何生成 source map
+ config.devtool = '#cheap-module-eval-source-map';
  config.devServer = {
    // 端口
    port: 8080,
    // 主機
    host: '0.0.0.0',
    // 使webpack錯誤顯示到頁面上
    overlay: {
      error: true,
    },
    // 自動打開瀏覽器
    // open: true,
    // 模塊熱替換,只更新更改的部分
+   hot: true
  };
  // 添加插件
+ config.plugins.push(
    // 熱模塊替換插件
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  );
}
  • 重啓
npm run dev

改變內容,頁面局部刷新。

配置完成

此時的文件內容:

// webpack.config.js

// path是nodejs中的一個基本包,用來處理路徑的
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const HTMLPlugin = require('html-webpack-plugin');
const webpack = require('webpack');

const isDev = process.env.NODE_ENV === 'development';

const config = {
  // 編譯目標是web平臺
  target: 'web',
  // 聲明入口,entry使用絕對路徑,保證不出錯誤
  entry: path.join(__dirname, 'src/index.js'),
  mode: 'production',
  // 出口
  output: {
    // 輸出打包文件名(將index.js以及其依賴的資源打包成bundle.js)
    filename: 'bundle.js',
    // 輸出路徑
    path: path.join(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(gif|jpg|jpeg|png|svg)$/,
        use: [
          {
            loader:  'url-loader',
            options: {
              limit: 1024,
              name: '[name]-aaa.[ext]'
            }
          }
        ]
      },
      {
        test: /\.styl/,
        use: [
          'style-loader',
          'css-loader',
          'stylus-loader'
        ]
      }
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: isDev ? '"development"' : '"production"'
      }
    }),
    new VueLoaderPlugin(),
    new HTMLPlugin()
  ]
}

// 若是是開發模式,則進行下列配置
if(isDev){
  // 控制是否生成,以及如何生成 source map
  config.devtool = '#cheap-module-eval-source-map';
  config.devServer = {
    // 端口
    port: 8080,
    // 主機
    host: '0.0.0.0',
    // 使webpack錯誤顯示到頁面上
    overlay: {
      error: true,
    },
    // 自動打開瀏覽器
    // open: true,
    // 模塊熱替換,只更新更改的部分
    hot: true
  };
  // 添加插件
  config.plugins.push(
    // 熱模塊替換插件
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  );
}

module.exports = config;

此時的文件內容:

// package.json

{
  "name": "vue-todo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "cross-env NODE_ENV=production webpack --config webpack.config.js",
    "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cross-env": "^5.2.0",
    "css-loader": "^2.1.1",
    "file-loader": "^3.0.1",
    "html-webpack-plugin": "^3.2.0",
    "style-loader": "^0.23.1",
    "stylus": "^0.54.5",
    "stylus-loader": "^3.0.2",
    "url-loader": "^1.1.2",
    "vue": "^2.6.10",
    "vue-loader": "^15.7.0",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.32.2",
    "webpack-dev-server": "^3.5.1"
  },
  "devDependencies": {
    "webpack-cli": "^3.3.2"
  }
}

到此時爲止,項目配置基本完成,以後寫業務邏輯。

vue介紹和項目實戰

vue是一個數據綁定的組件化的框架

數據綁定

vue文件開發方式

render方法

API重點:

  • 生命週期方法
  • computed

配置vuejsx寫法以及postcss

  • 在正式開發以前,安裝下列依賴:
npm i postcss-loader autoprefixer babel-loader babel-cor
  • 在項目根目錄下創建兩個配置文件:.babelrcpostcss.config.js
npm i babel-preset-env babel-plugin-transform-vue-jsx
// postcss.config.js

const autoprefixer = require('autoprefixer');

module.exports = {
  plugins: [
    autoprefixer()
  ]
};
// .babelrc

{
  "presets": [
    "env"
  ],
  "plugins": [
    "transform-vue-jsx"
  ]
}
  • webpack.config.js中配置
{
  test: /\.jsx$/,
  loader: 'babel-loader'
},

實現todo應用界面

src目錄下新建src/components目錄,新建Header.vueTodo.vueItem.vueTabs.vueFooter.jsx這些組件和global.styl

目錄結構以下:

// index.js

/**
 * 入口文件
 */
import Vue from 'vue';
import App from './App.vue';

import '../src/assets/styles/global.styl';

const root = document.createElement('div');
document.body.appendChild(root);

// 建立Vue對象,將App組件掛載到root節點
new Vue({
  render: (h) => h(App)
}).$mount(root);
<!-- Header.vue-->

<template>
  <header class="main-header">
    <h1>ToDo</h1>
  </header>
</template>

<style lang="stylus" scoped>
  .main-header {
    text-align: center;

    h1 {
      font-size: 100px;
      color: palevioletred;
      font-weight: 100px;
      margin: 50px;
    }
  }
</style>
<!--Todo.vue-->

<template>
  <section class="real-app">
    <input
      type="text"
      class="add-input"
      autofocus="autofocus"
      placeholder="添加任務"
      @keyup.enter="addTodo"
    >
    <Item
      :todo="todo"
      v-for="todo in filteredTodos"
      :key="todo.id"
      @del="deleteTodo"
    />
    <Tabs
      :filter="filter"
      :todos="todos"
      @toggle="toggleFilter"
      @clearAllCompleted="clearAllCompleted"
    />
  </section>
</template>

<script>
  import Item from './Item.vue';
  import Tabs from './Tabs.vue';
  let id = 0;
  export default {
    data(){
      return {
        todos: [],
        filter: "all"
      }
    },
    components: {
      Item,
      Tabs
    },
    computed: {
      filteredTodos(){
        // 若是 filter的狀態爲all,顯示全部的todos
        if(this.filter === 'all'){
          return this.todos;
        }
        // 篩選的方法就是讓this.filter等於todo.completed,
        // 可是filter是一個字符串,因此加下面這個條件判斷,
        // 獲得一個true或false的值,用它去過濾todos列表
        const completed = this.filter === 'completed';
        // filter的結果返回true則顯示,返回false則不顯示
        return this.todos.filter(todo => completed === todo.completed)
      }
    },
    methods: {
      addTodo(e) {
        this.todos.unshift({
          id: id++,
          content: e.target.value.trim(),
          completed: false
        });
        e.target.value = '';
      },
      deleteTodo(id){
        this.todos.splice(this.todos.findIndex(todo => todo.id === id), 1);
      },
      toggleFilter(state){
        this.filter = state;
      },
      clearAllCompleted(){
        this.todos = this.todos.filter(todo => !todo.completed);
      }
    }
  }
</script>

<style scoped>
  .real-app {
    width: 600px;
    margin: 0 auto;
    box-shadow: 0 0 5px #666;
  }

  .add-input {
    position: relative;
    width: 100%;
    min-height: 40px;
    padding-left: 60px;
    line-height: 40px;
    font-size: 16px;
    border: 3px solid pink;
    box-sizing: border-box;
    background-color: ghostwhite;
  }
</style>
<!--Item.vue-->

<template>
  <div :class="['todo-item', todo.completed ? 'completed' : '']">
    <input
      type="checkbox"
      id="toggle"
      v-model="todo.completed"
    >
    <label>{{todo.content}}</label>
    <button class="del" @click="deleteTodo"></button>
  </div>
</template>

<script>
  export default {
    props: {
      todo: {
        type: Object,
        required: true,
      }
    },
    methods: {
      deleteTodo() {
        // 觸發事件,在父組件中用@del="deleteTodo"的方式監聽,
        // 實現父子組件間事件的解耦
        this.$emit('del', this.todo.id);
      }
    }
  }
</script>

<style lang="stylus" scoped>
  .todo-item {
    position: relative;
    background-color: white;
    font-size: 24px;
    border-bottom: 1px solid pink;

    &:hover {
      .del:after {
        content: 'x';
      }
    }

    label {
      white-space: pre-line;
      word-break: break-all;
      padding: 15px 60px 15px 15px;
      margin-left: 45px;
      display: block;
      line-height: 1.2;
      transition: color 0.4s;
    }

    &.completed {
      label {
        color: #d9d9d9;
        text-decoration: line-through;
      }
    }
  }

  #toggle {
    position: absolute;
    text-align: center;
    width: 20px;
    height: 20px;
    left: 10px;
    top: 0;
    bottom: 0;
    margin: auto 0;
    border: none;
    outline: none;
  }

  .del {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 10px;
    width: 40px;
    height: 40px;
    margin: auto 0;
    color: red;
    background-color: transparent;
    font-size: 30px;
    border-width: 0;
    cursor: pointer;
    outline: none;
  }
</style>
<!--Tabs.vue-->

<template>
  <div class="helper">
    <span class="left">{{unFinishedTodoLength}} items left</span>
    <span class="tabs">
      <span
        v-for="state in states"
        :key="state"
        :class="[state, filter === state ? 'actived' : '']"
        @click="toggleFilter(state)"
      >
        {{state}}
      </span>
    </span>
    <span class="clear" @click="clearAllCompleted">Clear completed</span>
  </div>
</template>

<script>
  export default {
    props: {
      filter: {
        type: String,
        required: true,
      },
      todos: {
        type: Array,
        required: true
      }
    },
    data() {
      return {
        states: ['all', 'active', 'completed']
      }
    },
    computed: {
      unFinishedTodoLength() {
        return this.todos.filter(todo => !todo.completed).length;
      }
    },
    methods: {
      toggleFilter(state) {
        this.$emit('toggle', state);
      },
      clearAllCompleted() {
        this.$emit('clearAllCompleted');
      }
    }
  }
</script>

<style lang="stylus" scoped>
  .helper {
    font-family: Georgia, serif;
    font-weight: 100;
    display: flex;
    justify-content: space-between;
    padding: 5px 0;
    line-height: 30px;
    background-color: #fff;
    font-size: 14px;
  }

  .left, .clear, .tabs {
    padding: 0 10px;
    box-sizing: border-box;
  }

  .left, .clear {
    width: 150px;
  }

  .left {
    text-align: left;
  }

  .clear {
    text-align: right;
    cursor: pointer;
  }

  .tabs {
    width: 200px;
    display: flex;
    justify-content: space-around;

    * {
      display: inline-block;
      padding 0 10px;
      cursor: pointer;
      border: 1px solid rgba(175, 47, 47, 0);

      &.actived {
        border-color: rgba(175, 47, 47, 0.4);
        border-radius: 5px;
      }
    }
  }
</style>
<!--App.vue-->

<template>
  <div id="app">
    <div id="cover"></div>
    <Header/>
    <Todo/>
    <Footer/>
  </div>
</template>

<script>
  import Header from './components/Header.vue';
  import Footer from './components/Footer.jsx';
  import Todo from './components/Todo.vue';

  export default {
    components: {
      Header,
      Footer,
      Todo
    }
  }
</script>

<style scoped>
  #app {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
  }

  /* 實現虛化效果*/
  #cover {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    background-color: #999;
    opacity: .6;
    z-index: -1;
  }
</style>
// Footer.jsx

import '../assets/styles/footer.styl';

export default {
  data() {
    return {
      author: 'MandyShen'
    }
  },
  render() {
    return (
      <div id="footer">
        <span>Written by {this.author}</span>
      </div>
    )
  }
};
//footer.styl

#footer{
  margin-top 40px
  text-align center
  color mediumvioletred
  font-size 18px
  text-shadow 0 1px 0 #2b81af
}

webpack配置優化

webpack配置css單獨分離打包

npm i extract-text-webpack-plugin

做用是將非javascript代碼的資源單獨打包成一個靜態資源文件。

  • webpack.config.js中引入
// webpack.config.js

const ExteactPlugin = require('extract-text-webpack-plugin');

webpack區分打包類庫代碼及hash優化

// webpack.config.js

// path是nodejs中的一個基本包,用來處理路徑的
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const HTMLPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const ExtractPlugin = require('extract-text-webpack-plugin');

const isDev = process.env.NODE_ENV === 'development';

const config = {
  // 編譯目標是web平臺
  target: 'web',
  // 聲明入口,entry使用絕對路徑,保證不出錯誤
  entry: path.join(__dirname, 'src/index.js'),
  // mode: 'production',
  // 出口
  output: {
    // 輸出打包文件名(將index.js以及其依賴的資源打包成bundle.js)
    filename: 'bundle.[hash:8].js',
    // 輸出路徑
    path: path.join(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.jsx$/,
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(gif|jpg|jpeg|png|svg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 1024,
              name: '[name]-aaa.[ext]'
            }
          }
        ]
      },

    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: isDev ? '"development"' : '"production"'
      }
    }),
    new VueLoaderPlugin(),
    new HTMLPlugin()
  ]
}

// 若是是開發模式,則進行下列配置
if (isDev) {
  config.module.rules.push({
    test: /\.styl/,
    use: [
      'style-loader',
      'css-loader',
      {
        loader: 'postcss-loader',
        options: {
          sourceMap: true,
        }
      },
      'stylus-loader'
    ]
  });
  // 控制是否生成,以及如何生成 source map
  config.devtool = '#cheap-module-eval-source-map';
  config.devServer = {
    // 端口
    port: 8080,
    // 主機
    host: '0.0.0.0',
    // 使webpack錯誤顯示到頁面上
    overlay: {
      error: true,
    },
    // 自動打開瀏覽器
    // open: true,
    // 模塊熱替換,只更新更改的部分
    hot: true
  };
  // 添加插件
  config.plugins.push(
    // 熱模塊替換插件
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  );
} else {
  config.entry = {
    app: path.join(__dirname, 'src/index.js'),
    vendor: ['vue']
  };
  config.output.filename = '[name].[chunkhash:8].js';
  config.module.rules.push(
    {
      test: /\.styl/,
      use: ExtractPlugin.extract({
        fallback: 'style-loader',
        use: [
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              sourceMap: true,
            }
          },
          'stylus-loader'
        ]
      })
    },
  );
  config.plugins.push(
    new ExtractPlugin('styles.[hash:8].css')
  );
  config.optimization = {
    splitChunks: {
      cacheGroups: {
        commons: {
          chunks: 'initial',
          minChunks: 2, maxInitialRequests: 5,
          minSize: 0
        },
        vendor: {
          test: /node_modules/,
          chunks: 'initial',
          name: 'vendor',
          priority: 10,
          enforce: true
        }
      }
    },
    runtimeChunk: true
  }
}

module.exports = config;
相關文章
相關標籤/搜索