vue 快速入門 系列 —— vue loader 擴展

其他章節請看:

vue 快速入門 系列

vue loader 擴展

vue loader一文中,我們學會了從零搭建一個簡單的,用於單文件組件開發的腳手架。本篇將在此基礎上繼續引入一些常用的庫:vue-router、vuex、axios、mockjs、i18n、jquery、lodash。

環境準備

Tip: 此環境本質就是「vue loader」一文最終生成的代碼,略微精簡一下:刪除不必要的文件、wepback.config.js 注釋掉 eslint 以及自定義 loader。

項目結構:

vue-loader-test        
  - src                 // 項目源碼
    - index.html        // 頁面模板
    - index.js          // 入口
  - package.json        // 存放了項目依賴的包
  - webpack.config.js   // webpack配置文件
// index.html

<!DOCTYPE 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, initial-scale=1.0">
    <title>Document</title>
</head>
<body></body>
</html>
// index.js

console.log('hello');
// package.json

{
    "name": "vue-loader-test",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "webpack",
        "dev": "webpack-dev-server"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
        "@babel/preset-env": "^7.14.7",
        "babel-loader": "^8.2.2",
        "css-loader": "^5.2.4",
        "eslint": "^7.30.0",
        "eslint-config-airbnb-base": "^14.2.1",
        "eslint-plugin-vue": "^7.12.1",
        "eslint-webpack-plugin": "^2.5.4",
        "file-loader": "^6.2.0",
        "html-webpack-plugin": "^4.5.2",
        "less": "^4.1.1",
        "less-loader": "^7.3.0",
        "mini-css-extract-plugin": "^1.6.2",
        "node-sass": "^6.0.1",
        "postcss-loader": "^4.3.0",
        "postcss-preset-env": "^6.7.0",
        "pug": "^3.0.2",
        "pug-plain-loader": "^1.1.0",
        "sass-loader": "^10.2.0",
        "style-loader": "^2.0.0",
        "stylus": "^0.54.8",
        "stylus-loader": "^4.3.3",
        "ts-loader": "^7.0.5",
        "typescript": "^4.3.5",
        "url-loader": "^4.1.1",
        "vue": "^2.6.14",
        "vue-loader": "^15.9.7",
        "vue-template-compiler": "^2.6.14",
        "webpack": "^4.46.0",
        "webpack-cli": "^3.3.12",
        "webpack-dev-server": "^3.11.2"
    }
}
// webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {
    VueLoaderPlugin
} = require('vue-loader');

const process = require('process');
process.env.NODE_ENV = 'production'
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
const postcssLoader = {
    loader: 'postcss-loader',
    options: {
        // postcss 只是個平台,具體功能需要使用插件
        postcssOptions: {
            plugins: [
                [
                    "postcss-preset-env",
                    {
                        browsers: 'ie >= 8, chrome > 10',
                    },
                ],
            ]
        }
    }
}
module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                oneOf: [
                    // 這裡匹配 `<style module>`
                    {
                        resourceQuery: /module/,
                        use: [
                            'vue-style-loader',
                            {
                                loader: 'css-loader',
                                options: {
                                    // 開啟 CSS Modules
                                    modules: {
                                        // 自定義生成的類名
                                        localIdentName: '[local]_[hash:base64:8]'
                                    }

                                }
                            }
                        ]
                    },
                    {
                        use: [
                            process.env.NODE_ENV !== 'production'
                                ? 'vue-style-loader'
                                : MiniCssExtractPlugin.loader,
                            'css-loader'
                        ]
                    },
                ]
            },
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {
                    hotReload: true // 關閉熱重載
                }
            },
            {
                test: /\.(png|jpg|gif)$/i,
                use: [{
                    loader: 'url-loader',
                    options: {
                        // 調整的比 6.68 要小,這樣圖片就不會打包成 base64 
                        limit: 1024 * 6,
                        esModule: false,
                    },
                },],
            },
            {
                test: /\.scss$/,
                use: [
                    'vue-style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.sass$/,
                use: [
                    'vue-style-loader',
                    'css-loader',
                    {
                        loader: 'sass-loader',
                        options: {
                            // sass-loader version >= 8
                            sassOptions: {
                                indentedSyntax: true
                            },
                            additionalData: `$size: 3em;`,
                        }
                    }
                ]
            },
            {
                test: /\.less$/,
                use: [
                    'vue-style-loader',
                    // +
                    {
                        loader: 'css-loader',
                        options: {
                            // 開啟 CSS Modules
                            modules: {
                                localIdentName: '[local]_[hash:base64:8]'
                            }

                        }
                    },
                    postcssLoader,
                    'less-loader'
                ]
            },
            {
                test: /\.styl(us)?$/,
                use: [
                    'vue-style-loader',
                    'css-loader',
                    'stylus-loader'
                ]
            },
            {
                test: /\.js$/,
                // exclude: /node_modules/,
                exclude: file => (
                    /node_modules/.test(file) &&
                    !/\.vue\.js/.test(file)
                ),
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            ['@babel/preset-env']
                        ]
                    }
                }
            },
            {
                test: /\.ts$/,
                loader: 'ts-loader',
                options: { appendTsSuffixTo: [/\.vue$/] }
            },
            {
                test: /\.pug$/,
                loader: 'pug-plain-loader'
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }),
        new VueLoaderPlugin(),
        new MiniCssExtractPlugin(),
    ],
    mode: 'development',
    devServer: {
        hot: true,
        open: true,
        contentBase: path.join(__dirname, 'dist'),
        compress: true,
        port: 9000,
    },
    resolve: {
        alias: {
            '@': path.resolve(__dirname, 'src/'),
        },
        extensions: ['.ts', '.js'],
    },
};

安裝依賴,啟動服務,如果在自動打開的網頁的控制台中輸出:hello,則說明環境準備就緒。

> npm i
// 啟動服務
> npm run dev

vue-router

Tip:由於 vue-router 官網的”起步”章節,不是以單頁面組件的形式介紹的,所以筆者另外參考了 vue-cli 創建帶有 vue-router 的項目。

將 vue-router 引入工程,步驟如下:

> npm i vue-router@3

創建 App.vue,包含一個導航,導航指向兩個組件,分別是 Home 和 About:

// src/App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

在 src/views 文件夾中創建兩個組件:

// views/Home.vue

<template>
    <div>
        i am Home
    </div>
</template>
// views/About.vue

<template>
    <div>
        i am About
    </div>
</template>

創建路由,並將路由統一放在 src/router 文件夾中:

// router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
// 導入 Home 組件
import Home from '../views/Home.vue'

// 在一個模塊化工程中使用 vue-router,必須通過 Vue.use() 明確地安裝路由功能
Vue.use(VueRouter)

// 定義路由 map
const routes = [
    {
        path: '/',
        name: 'Home',
        component: Home
    },
    {
        path: '/about',
        name: 'About',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
    }
]

// 創建路由實例
const router = new VueRouter({
    routes
})

export default router

最後在 src/index.js 中使用路由:

// index.js

import Vue from 'vue';
import router from './router';
import App from './App.vue';

new Vue({
    // 通過 router 配置參數注入路由,從而讓整個應用都有路由功能
    router,
    el: 'body',
    render: (h) => h(App),
});

重啟服務,頁面顯示如下:

Home | About
i am home

默認是導航是 Home,點擊 About,主體內容則變成i am About

vuex

Tip: 參考 vue-cli 創建的帶有 vuex 的項目。

將 vuex 引入工程,步驟如下:

> npm i vuex@3

創建 vuex 文件,統一將 vuex 放在 src/store 文件夾中:

// store/index.js

import Vue from 'vue';
import Vuex from 'vuex';

// 在一個模塊化的打包系統中,必須顯式地通過 Vue.use() 來安裝 Vuex
Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        count: 0,
    },
    mutations: {
    },
    actions: {
    },
    modules: {
    },
});

通過 store 配置參數注入 vuex,從而讓整個應用都有 vuex 功能:

// src/index.js

+ import store from './store';

new Vue({
  // 為了在 Vue 組件中訪問 this.$store property,需要為 Vue 實例提供創建好的 store
  // Vuex 提供了一個從根組件向所有子組件,以 store 選項的方式「注入」該 store 的機制
  + store,
  el: 'body',
  render: (h) => h(App),
});

在子組件(Home.vue)中訪問 store:

// views/Home.vue

...
<script>
export default {
  created() {
    console.log(this.$store.state.count);
  },
};
</script>

重啟服務,瀏覽器控制台輸出 0。

axios

Tip: 參考 vue-cli 安裝 axios 插件後生成的代碼。

將 axios 引入工程,步驟如下:

> npm i axios@0

創建 axios.js:

// src/plugins/axios.js

import Vue from 'vue';
import axios from 'axios';

// Full config:  //github.com/axios/axios#request-config
// axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || '';
// axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

const config = {
    // baseURL: process.env.baseURL || process.env.apiUrl || ""
    // timeout: 60 * 1000, // Timeout
    // withCredentials: true, // Check cross-site Access-Control
};

const _axios = axios.create(config);

_axios.interceptors.request.use(
    (config) =>
        // Do something before request is sent
        config,
    (error) =>
        // Do something with request error
        Promise.reject(error)
  ,
);

// Add a response interceptor
_axios.interceptors.response.use(
    (response) =>
        // Do something with response data
        response,
    (error) =>
        // Do something with response error
        Promise.reject(error)
  ,
);

// Vue.use(Plugin) 會調用此方法
// 方法中會將 _axios 暴露給 Vue.axios、window.axios
// 並可以通過 Vue 的實例上的 axios 和 $axios 訪問到 _axios
Plugin.install = function (Vue, options) {
    Vue.axios = _axios;
    window.axios = _axios;
    Object.defineProperties(Vue.prototype, {
        axios: {
            get() {
                return _axios;
            },
        },
        $axios: {
            get() {
                return _axios;
            },
        },
    });
};

Vue.use(Plugin);

export default Plugin;

在入口文件中引入 axios.js:

// index.js
import './plugins/axios';

在 Home.vue 中使用 axios:

// Home.vue
...
<script>
export default {
  created() {
    console.log('this.$axios: ', this.$axios);
  },
};
</script>

重啟服務,瀏覽器控制台輸出:

this.$axios:  ƒ wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
}

至此,axios.js 已成功引入到我們的工程。在 mockjs 中我們還會用到 axios。

mockjs

:在 npm 中查看 vue-cli-plugin-mock,好似需要在 vue.config.js 中配置。我們的工程不是通過 vue-cli 創建的,所以這裡直接使用 mockjs。

將 mockjs 引入工程,步驟如下:

> npm i -D mockjs@1

創建 mock:

// src/mock/index.js

import Mock from 'mockjs';

// 定義了一個請求攔截
Mock.mock('/list', {
    'name': '@name',
    'age|10-20': 10,
    'birthday': '@date("yyyy-MM-dd")'
});

在入口文件中引入 mock:

// index.js

// 會自動加載 mock 下的 index.js
import './mock'

最後測試 mock 是否能攔截 /list 請求:

// Home.vue
...
<script>
export default {
  created() {
    // 發送請求
    this.$axios.get('/list').then(res => {
        console.log('res.data: ', res.data);
    })
  },
};
</script>

重啟服務,瀏覽器控制台輸出如下類似數據:

res.data:  {name: "Ruth Brown", age: 14, birthday: "1973-08-18"}

Tip: 在實際項目中,mock 還需要進一步配置,比如可以在 mock/index.js 中加載 mock 文件夾中其他的 mock 文件,方便擴展。

i18n

Tip:參考 vue i18n 官方文檔 和 vue-cli 安裝 i18n 插件後生成的代碼。

由於我們是 vue 項目,所以就選用 vue i18n。

Vue I18n 是 Vue.js 的國際化插件。它可以輕鬆地將一些本地化功能集成到你的 Vue.js 應用程序中。

將 i18n 引入工程,步驟如下:

> npm i vue-i18n@8

新建中英文兩個 json 文件,用於存放需要翻譯的字段(中文和英文),並統一放在 src/language 目錄中。

// en.json
{
    "apple": "Apple"
}
// zh.json
{
    "apple": "蘋果"
}

新建 i18n 模塊,創建並導出 i18n 實例:

// src/i18n.js

import Vue from 'vue'
import VueI18n from 'vue-i18n'

Vue.use(VueI18n)

function loadLocaleMessages() {
    // 通過 require.context() 函數來創建自己的 context
    // 可以給這個函數傳入三個參數:一個要搜索的目錄,一個標記表示是否還搜索其子目錄, 以及一個匹配文件的正則表達式。
    // 返回一個函數,函數有三個屬性:resolve, keys, id。
    // keys 也是一個函數,它返回一個數組,由所有可能被此 context module 處理的請求
    const locales = require.context('./language', true, /\.json$/i)
    const messages = {}
    locales.keys().forEach(file => {
        const matched = file.match(/([A-Za-z0-9-_]+)\./i)
        const locale = matched[1]
        messages[locale] = locales(file)
    })
    // messages:  {en: {…}, zh: {…}}
    console.log('messages: ', messages);
    return messages
}

export default new VueI18n({
    locale: process.env.VUE_APP_I18N_LOCALE || 'en',
    fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
    messages: loadLocaleMessages()
})

在入口文件中導入 i18n 實例,並配置到 Vue 實例中:

// index.js
...
import i18n from './i18n'

new Vue({
    ...
    i18n,
    el: 'body',
    render: (h) => h(App),
});

接着就可以在 vue 組件中使用:

// Home.vue

<template>
  <div>
    i am home
    <p>{{ $t("apple") }}</p>
  </div>
</template>

重啟服務,頁面顯示 」Apple「:

Home | About
i am home
Apple

為什麼顯示的是英文,而非中文?由 i18n.js 中的這段代碼決定:

export default new VueI18n({
    locale: process.env.VUE_APP_I18N_LOCALE || 'en',
    fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
    messages: loadLocaleMessages()
})

process 是 node 中的對象,而這裡是在 .js 中使用。可以這麼理解:

locale: process.env.VUE_APP_I18N_LOCALE || 'en',

等效於

locale: undefined || 'en',

所以是英文。如果想在模塊中使用 process.env.VUE_APP_I18N_LOCALE,還需要一些配置。

// webpack.config.js
const webpack = require('webpack');
 
module.exports = {
    ...
    plugins: [
        new webpack.DefinePlugin({
            // DefinePlugin 會直接將內容替換了,而不是一個字符串
            // 所以我們不能直接寫 'zh',經常會這麼寫:JSON.stringify('zh')
            'process.env.VUE_APP_I18N_LOCALE': JSON.stringify('zh'),
            'process.env.VUE_APP_I18N_FALLBACK_LOCALE': JSON.stringify('zh'),
        })
    ]
};

Tip: DefinePlugin 允許在編譯時將你代碼中的變量替換為其他值或表達式

重啟服務,頁面顯示 」蘋果「:

Home | About
i am home
蘋果

在瀏覽器中找到如下代碼:

{\n  locale: \"zh\" || false,\n  fallbackLocale: \"zh\" || false,\n  messages: loadLocaleMessages()\n}

i18n.js 中的 process.env.VUE_APP_I18N_LOCALE 被替換成 zh。

jquery

將 jquery 引入工程,步驟如下:

> npm i -D jquery@3

配置別名:

// webpack.config.js

module.exports = {
  resolve: {
    alias: {
      'jquery': path.resolve(__dirname, './node_modules/jquery/dist/jquery.min.js')
    },
  },

在需要使用的地方引用:

// Home.vue
...
<script>
import $ from 'jquery';
export default {
  mounted() {
      $('#app').css('color', 'blue')
  },
};
</script>

重啟服務,頁面中文字變為藍色,說明 jquery 引入成功。

lodash

將 lodash 引入工程,步驟如下:

> npm i -D lodash@4

在 Home.vue 中使用 lodash:

// Home.vue
...
<script>
// webpack 模塊 能以各種方式表達它們的依賴關係
// 比如 es 的 import、commonjs 的 require等等
let _ = require('lodash')
export default {
  mounted() {
      // 調用 lodash 的 random 方法
      console.log(_.random(1,10))
  },
};
</script>

重啟服務,瀏覽器控制台輸出1~10之間一個隨機數,說明 lodash 引入成功。

Tip: jquery 同樣可以使用這種方式

其他章節請看:

vue 快速入門 系列

Tags: