手把手教你從0開始搭建一個vue項目(完結)

前言

上一節webpack實戰之(手把手教你從0開始搭建一個vue項目)最后我們完成了css樣式的配置:

webpack.config.js:

const path = require("path");
const config = new (require("webpack-chain"))();
const isDev = process.env.WEBPACK_DEV_SERVER;
config
    .context(path.resolve(__dirname, ".")) //webpack上下文目錄為項目根目錄
    .entry("app") //入口文件名稱為app
        .add("./src/main.ts") //入口文件為./src/main.ts
        .end()
    .output
        .path(path.join(__dirname,"./dist")) //webpack輸出的目錄為根目錄的dist目錄
        .filename("[name].[hash:8].js")
        .end()
    .resolve
        .extensions
            .add(".js").add(".jsx").add(".ts").add(".tsx").add(".vue") //配置以.js等結尾的文件當模塊使用的時候都可以省略后綴
            .end()
        .end()
    .module
        .rule("type-script")
            .test(/\.tsx?$/) //loader加載的條件是ts或tsx后綴的文件
            .use("ts-loader")
                .loader("ts-loader")
                .options({ //ts-loader相關配置
                    transpileOnly: true,
                    appendTsSuffixTo: ['\\.vue$']
                })
                .end()
            .end()
        .rule("vue")
            .test(/\.vue$/)// 匹配.vue文件
                .use("vue-loader")
                .loader("vue-loader")
                .end()
            .end()
        .rule("sass")
            .test( /\.(sass|scss)$/)//sass和scss文件
            .use("extract-loader")//提取css樣式到單獨css文件
                .loader(require('mini-css-extract-plugin').loader)
                .options({
                    hmr: isDev //開發環境開啟熱載
                })
                .end()
            .use("css-loader")//加載css模塊
                .loader("css-loader")
                .end()
            .use("postcss-loader")//處理css樣式
                .loader("postcss-loader")
                .options( {
                    config: {
                       path: path.resolve(__dirname, "./postcss.config.js")
                    }
                })
                .end()
            .use("sass-loader")//sass語法轉css語法
                .loader("sass-loader")
                .end()
            .end()
        .end()
    .plugin("vue-loader-plugin")//vue-loader必須要添加vue-loader-plugin
        .use(require("vue-loader").VueLoaderPlugin,[])
        .end()
    .plugin("html")// 添加html-webpack-plugin插件
        .use(require("html-webpack-plugin"),[{
            template: path.resolve(__dirname,"./public/index.html"), //指定模版文件
            chunks:["app"], //指定需要加載的chunk
            inject: "body" //指定script腳本注入的位置為body
        }])
        .end()
    .plugin("extract-css")//提取css樣式到單獨css文件
        .use(require('mini-css-extract-plugin'), [{
            filename: "css/[name].css",
            chunkFilename: "css/[name].css"
        }])
        .end()
    .devServer
        .host("0.0.0.0") //為了讓外部服務訪問
        .port(8090) //當前端口號
        .hot(true) //熱載
        .open(true) //開啟頁面
module.exports = config.toConfig();
css配置

配置

babel

看一下我們當前項目中的測試代碼,src/app.vue:

<template>
    <div class="app-container">{{this.msg}}</div>
</template>

<script lang="ts">
  import {Vue, Component} from "vue-property-decorator";

  @Component
  export default class App extends Vue {
    msg="hello world";
    user={
      name: "yasin"
    };
    created(){
        const name=this.user?.name;
        console.log("name");
    }
  }
</script>

<style scoped lang="scss">
.app-container{
    color: red;
}
</style>

比如我們需要用到最新的optional-chaining語法:

 const name=this.user?.name;

我們試著運行一下我們的demo:

...
ERROR in ./src/app.vue?vue&type=script&lang=ts& (./node_modules/ts-loader??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./src/app.vue?vue&type=script&lang=ts&) 12:31
Module parse failed: Unexpected token (12:31)
File was processed with these loaders:
 * ./node_modules/ts-loader/index.js
 * ./node_modules/vue-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
|     }
|     created() {
>         const name = this.user?.name;
|         console.log("name");
|     }
 @ ./src/app.vue?vue&type=script&lang=ts& 1:0-160 1:176-179 1:181-338 1:181-338
 @ ./src/app.vue
 @ ./src/main.ts
 @ multi ./src/main.ts

可以發現,報錯了! 提示我們“該語法解析器不能解析”,有小伙伴可能發現了,在我們demo中我們還用到了:

 import {Vue, Component} from "vue-property-decorator";

  @Component
  export default class App extends Vue {

裝飾器語法,那demo之前也沒報錯啊,為什么呢? 因為我們用的是ts的解析器,ts解析器是可以解析裝飾器這種語法的:

<script lang="ts">

我們把lang="ts"去掉試試:

<template>
    <div class="app-container">{{this.msg}}</div>
</template>

<script lang="ts">
  import {Vue, Component} from "vue-property-decorator";

  @Component
  export default class App extends Vue {
    msg="hello world";
    user={
      name: "yasin"
    };
    created(){
        const name=this.user?.name;
        console.log("name");
    }
  }
</script>

<style scoped lang="scss">
.app-container{
    color: red;
}
</style>
ERROR in ./src/app.vue?vue&type=script&lang=js& (./node_modules/vue-loader/lib??vue-loader-options!./src/app.vue?vue&type=script&lang=js&) 8:0
Module parse failed: Unexpected character '@' (8:0)
File was processed with these loaders:
 * ./node_modules/vue-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
| import {Vue, Component} from "vue-property-decorator";
| 
> @Component
| export default class App extends Vue {
|   msg="hello world";
 @ ./src/app.vue?vue&type=script&lang=js& 1:0-115 1:131-134 1:136-248 1:136-248
 @ ./src/app.vue
 @ ./src/main.ts
 @ multi ./src/main.ts

ok! 可以看到,這次報的是裝飾器語法解析失敗了,知道原因后,我們該怎么解決呢? 看過前面babel系列文章的童鞋應該是立馬就想到了babel,那么接下來我們就配置一下babel。

@babel/core(babel核心)

yarn add -D @babel/core || npm install -D @babel/core

@babel/preset-env (env預設)

做es6語法轉換、添加polyfill。

yarn add -D @babel/preset-env || npm install -D @babel/preset-env

@babel/plugin-transform-runtime (運行時插件)

添加一些babel轉換時候的幫助函數。

yarn add -D @babel/plugin-transform-runtime || npm install -D @babel/plugin-transform-runtime

babel-loader (es加載器)

yarn add -D babel-loader || npm install -D babel-loader

core-js (preset-env設置polyfill的依賴)

yarn add -D core-js || npm install -D core-js

ok, 安裝完babel的一些依賴后,我們開始配置webpack,首先是對js、jsx的babel配置:

 .module
        .rule('js')
            .test(/\.m?jsx?$/) //對mjs、mjsx、js、jsx文件進行babel配置
            .exclude
                .add(filepath => {
                    // Don't transpile node_modules
                    return /node_modules/.test(filepath)
                })
                .end()
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .end()

然后是ts、tsx的配置:

 .rule("type-script")
            .test(/\.tsx?$/) //loader加載的條件是ts或tsx后綴的文件
            .use("babel-loader")
                .loader("babel-loader")
                .end()

整個配置文件,webpack.config.js:

const path = require("path");
const config = new (require("webpack-chain"))();
const isDev = process.env.WEBPACK_DEV_SERVER;
config
    .context(path.resolve(__dirname, ".")) //webpack上下文目錄為項目根目錄
    .entry("app") //入口文件名稱為app
        .add("./src/main.ts") //入口文件為./src/main.ts
        .end()
    .output
        .path(path.join(__dirname,"./dist")) //webpack輸出的目錄為根目錄的dist目錄
        .filename("[name].[hash:8].js")
        .end()
    .resolve
        .extensions
            .add(".js").add(".jsx").add(".ts").add(".tsx").add(".vue") //配置以.js等結尾的文件當模塊使用的時候都可以省略后綴
            .end()
        .end()
    .module
        .rule('js')
            .test(/\.m?jsx?$/) //對mjs、mjsx、js、jsx文件進行babel配置
            .exclude
                .add(filepath => {
                    // Don't transpile node_modules
                    return /node_modules/.test(filepath)
                })
                .end()
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .end()
        .rule("type-script")
            .test(/\.tsx?$/) //loader加載的條件是ts或tsx后綴的文件
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .use("ts-loader")
                .loader("ts-loader")
                .options({ //ts-loader相關配置
                    transpileOnly: true,
                    appendTsSuffixTo: ['\\.vue$']
                })
                .end()
            .end()
        .rule("vue")
            .test(/\.vue$/)// 匹配.vue文件
                .use("vue-loader")
                .loader("vue-loader")
                .end()
            .end()
        .rule("sass")
            .test( /\.(sass|scss)$/)//sass和scss文件
            .use("extract-loader")//提取css樣式到單獨css文件
                .loader(require('mini-css-extract-plugin').loader)
                .options({
                    hmr: isDev //開發環境開啟熱載
                })
                .end()
            .use("css-loader")//加載css模塊
                .loader("css-loader")
                .end()
            .use("postcss-loader")//處理css樣式
                .loader("postcss-loader")
                .options( {
                    config: {
                       path: path.resolve(__dirname, "./postcss.config.js")
                    }
                })
                .end()
            .use("sass-loader")//sass語法轉css語法
                .loader("sass-loader")
                .end()
            .end()
        .end()
    .plugin("vue-loader-plugin")//vue-loader必須要添加vue-loader-plugin
        .use(require("vue-loader").VueLoaderPlugin,[])
        .end()
    .plugin("html")// 添加html-webpack-plugin插件
        .use(require("html-webpack-plugin"),[{
            template: path.resolve(__dirname,"./public/index.html"), //指定模版文件
            chunks:["app"], //指定需要加載的chunk
            inject: "body" //指定script腳本注入的位置為body
        }])
        .end()
    .plugin("extract-css")//提取css樣式到單獨css文件
        .use(require('mini-css-extract-plugin'), [{
            filename: "css/[name].css",
            chunkFilename: "css/[name].css"
        }])
        .end()
    .devServer
        .host("0.0.0.0") //為了讓外部服務訪問
        .port(8090) //當前端口號
        .hot(true) //熱載
        .open(true) //開啟頁面
module.exports = config.toConfig();

然后我們再次運行npm run dev指令:

ERROR in ./src/app.vue?vue&type=script&lang=js& (./node_modules/babel-loader/lib!./node_modules/vue-loader/lib??vue-loader-options!./src/app.vue?vue&type=script&lang=js&)
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: xxx/webpack-vue-demo/src/app.vue: Support for the experimental syntax 'decorators-legacy' isn't currently enabled (8:1):

   6 | import {Vue, Component} from "vue-property-decorator";
   7 | 
>  8 | @Component
     | ^
   9 | export default class App extends Vue {
  10 |   msg="hello world";
  11 |   user={
    at Parser._raise (xxx/webpack-vue-demo/node_modules/@babel/parser/lib/index.js:757:17)
    at Parser.raiseWithData (xxx/webpack-vue-demo/node_modules/@babel/parser/lib/index.js:750:17)

可以看到,還是報錯,這是為什么呢? 因為我們沒有對babel進行配置,我們在項目根目錄創建一個babel.config.js文件,然后進行babel配置:

babel.config.js

module.exports = {
  presets: [
    [
      "@babel/preset-env", //添加preset-env預設做語法轉換跟polyfill添加
      {
        corejs: 3,
        useBuiltIns: "usage",
        modules: false
      }
    ]
  ],
  plugins: [
    [
      "@babel/plugin-transform-runtime", //利用runtime做helpers跟regenerator設置
      {
        corejs: false,
        helpers: true,
        useESModules: false,
        regenerator: true,
        absoluteRuntime: "./node_modules"
      }
    ]
  ]
};

配置我就不詳細解說了,前面babel的文章中都有解析。

ok,我們再次運行npm run dev:

ERROR in ./src/app.vue?vue&type=script&lang=js& (./node_modules/babel-loader/lib!./node_modules/vue-loader/lib??vue-loader-options!./src/app.vue?vue&type=script&lang=js&)
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: xxx/webpack-vue-demo/src/app.vue: Support for the experimental syntax 'decorators-legacy' isn't currently enabled (8:1):

   6 | import {Vue, Component} from "vue-property-decorator";
   7 | 
>  8 | @Component
     | ^
   9 | export default class App extends Vue {
  10 |   msg="hello world";
  11 |   user={

ok, 還是報裝飾器語法無法解析,看過前面babel的同學應該是知道的,babel-preset-env默認是不添加“decorators”插件的,因為“decorators”還在提案2階段不穩定, 那我們需要在js中使用“decorators”怎么辦呢? 我們前面也有文章單獨介紹過“裝飾器”,看過的童鞋應該是知道的,

安裝“decorators”插件:

yarn add -D @babel/plugin-proposal-decorators || npm install -D @babel/plugin-proposal-decorators

安裝“類屬性”插件:

yarn add -D @babel/plugin-proposal-class-properties || npm install -D @babel/plugin-proposal-class-properties

配置babel.config.js:

module.exports = {
    presets: [
        [
            "@babel/preset-env", //添加preset-env預設做語法轉換跟polyfill添加
            {
                corejs: 3,
                useBuiltIns: "usage",
                modules: false
            }
        ]
    ],
    plugins: [
        ["@babel/plugin-proposal-decorators",{ //裝飾器插件
          legacy: true
        }],
        "@babel/plugin-proposal-class-properties", //類屬性插件
        [
            "@babel/plugin-transform-runtime", //利用runtime做helpers跟regenerator設置
            {
                corejs: false,
                helpers: true,
                useESModules: false,
                regenerator: true,
                absoluteRuntime: "./node_modules"
            }
        ]
    ]
};

注意??: 兩個插件的順序一定不要弄反哦!

然后我們再次運行npm run dev:

npm run dev
...
? ?wdm?: Compiled successfully.

可以看到,最后顯示編譯成功了,我就不截圖了,瀏覽器中應該會顯示“hello world”文字的。

同樣,我們把lang改成ts也是可以正常運行的:

src/app.vue

<template>
    <div class="app-container">{{this.msg}}</div>
</template>

<script lang="ts">
  import {Vue, Component} from "vue-property-decorator";

  @Component
  export default class App extends Vue {
    msg="hello world";
    user={
      name: "yasin"
    };
    created(){
        const name=this.user?.name;
        console.log("name");
    }
  }
</script>

<style scoped lang="scss">
.app-container{
    color: red;
}
</style>

因為我們在webpack的loader中給ts也添加了babel配置:

.rule("type-script")
            .test(/\.tsx?$/) //loader加載的條件是ts或tsx后綴的文件
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .use("ts-loader")
                .loader("ts-loader")
                .options({ //ts-loader相關配置
                    transpileOnly: true,
                    appendTsSuffixTo: ['\\.vue$']
                })
                .end()
            .end()

jsx

上一節我們還說了,我們的工程是需要支持js、jsx、ts、tsx的,現在js跟ts都是支持的,那么我們需要怎么支持jsx跟tsx呢?

ok! 在vue中vue官方已經給我們提供了一個第三方庫供我們使用jsx,下面我們安裝一下:

@vue/babel-preset-jsx

yarn add -D @vue/babel-preset-jsx || npm install -D @vue/babel-preset-jsx

然后直接配置到我們的babel配置文件即可,

babel.config.js:

module.exports = {
    presets: [
        "@vue/babel-preset-jsx", //vue支持jsx跟tsx
        [
            "@babel/preset-env", //添加preset-env預設做語法轉換跟polyfill添加
            {
                corejs: 3,
                useBuiltIns: "usage",
                modules: false
            }
        ]
    ],
    plugins: [
        ["@babel/plugin-proposal-decorators",{ //裝飾器插件
          legacy: true
        }],
        "@babel/plugin-proposal-class-properties", //類屬性插件
        [
            "@babel/plugin-transform-runtime", //利用runtime做helpers跟regenerator設置
            {
                corejs: false,
                helpers: true,
                useESModules: false,
                regenerator: true,
                absoluteRuntime: "./node_modules"
            }
        ]
    ]
};

沒錯! 就是這么簡單~ 為了驗證一下我們在src目錄下創建一個app.tsx文件,

src/app.tsx:

import {Vue, Component} from "vue-property-decorator";

@Component
export default class AppTsx extends Vue {
    msg = "hello tsx";

    render(h) {
        return (
            <div>{this.msg}</div>
        );
    }
}

代碼很簡單,我們直接用tsx輸出一個msg為“hello tsx”,然后我們修改一下main.ts入口文件,

main.ts:

import Vue from "vue";
// import App from "./app.vue";
import App from "./app";

new Vue({
  el: "#app",
  render: (h) => h(App)
});

然后我們運行npm run dev:

在這里插入圖片描述

可以看到,頁面中正常顯示了我們的內容。

eslint

eslint(eslint核心)

yarn add -D eslint || npm install -D eslint

eslint-loader (eslint加載器)

yarn add -D eslint-loader || npm install -D eslint-loader

eslint-plugin-vue (解析vue模版)

yarn add -D eslint-plugin-vue || npm install -D eslint-plugin-vue

@typescript-eslint/eslint-plugin(ts eslint插件)

yarn add -D @typescript-eslint/eslint-plugin || npm install -D @typescript-eslint/eslint-plugin

@typescript-eslint/parser (ts解析器,供eslint-plugin-vue解析vue模版以外語法使用)

yarn add -D @typescript-eslint/parser || npm install -D @typescript-eslint/parser

@vue/eslint-config-typescript (vue中對ts寫法的一些好的建議)

yarn add -D @vue/eslint-config-typescript || npm install -D @vue/eslint-config-typescript

eslint-plugin-prettier (比較好的一些js語法建議)

yarn add -D eslint-plugin-prettier || npm install -D eslint-plugin-prettier

@vue/eslint-config-prettier (vue推薦的比較好的寫法)

yarn add -D @vue/eslint-config-prettier || npm install -D @vue/eslint-config-prettier

ok, 我們已經安裝了我們需要的一些eslint依賴,然后我們去webpack配置中添加eslint-loader,

webpack.config.js:

const path = require("path");
const config = new (require("webpack-chain"))();
const isDev = !!process.env.WEBPACK_DEV_SERVER;
config
    .context(path.resolve(__dirname, ".")) //webpack上下文目錄為項目根目錄
    .entry("app") //入口文件名稱為app
        .add("./src/main.ts") //入口文件為./src/main.ts
        .end()
    .output
        .path(path.join(__dirname,"./dist")) //webpack輸出的目錄為根目錄的dist目錄
        .filename("[name].[hash:8].js")
        .end()
    .resolve
        .extensions
            .add(".js").add(".jsx").add(".ts").add(".tsx").add(".vue") //配置以.js等結尾的文件當模塊使用的時候都可以省略后綴
            .end()
        .end()
    .module
        .rule('js')
            .test(/\.m?jsx?$/) //對mjs、mjsx、js、jsx文件進行babel配置
            .exclude
                .add(filepath => {
                    // Don't transpile node_modules
                    return /node_modules/.test(filepath)
                })
                .end()
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .end()
        .rule("type-script")
            .test(/\.tsx?$/) //loader加載的條件是ts或tsx后綴的文件
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .use("ts-loader")
                .loader("ts-loader")
                .options({ //ts-loader相關配置
                    transpileOnly: true,
                    appendTsSuffixTo: ['\\.vue$']
                })
                .end()
            .end()
        .rule("vue")
            .test(/\.vue$/)// 匹配.vue文件
                .use("vue-loader")
                .loader("vue-loader")
                .end()
            .end()
        .rule("sass")
            .test( /\.(sass|scss)$/)//sass和scss文件
            .use("extract-loader")//提取css樣式到單獨css文件
                .loader(require('mini-css-extract-plugin').loader)
                .options({
                    hmr: isDev //開發環境開啟熱載
                })
                .end()
            .use("css-loader")//加載css模塊
                .loader("css-loader")
                .end()
            .use("postcss-loader")//處理css樣式
                .loader("postcss-loader")
                .options( {
                    config: {
                       path: path.resolve(__dirname, "./postcss.config.js")
                    }
                })
                .end()
            .use("sass-loader")//sass語法轉css語法
                .loader("sass-loader")
                .end()
            .end()
        .rule('eslint')//添加eslint-loader
            .exclude
                .add(/node_modules/)//校驗的文件除node_modules以外
                .end()
            .test(/\.(vue|(j|t)sx?)$/)//加載.vue、.js、.jsx、.ts、.tsx文件
                .use('eslint-loader')
                .loader(require.resolve('eslint-loader'))
                .options({
                    emitWarning: true, //出現警告是否終止webpack編譯
                    emitError: !isDev, //生成環境編譯報錯
                })
                .end()
            .end()
         .end()
    .plugin("vue-loader-plugin")//vue-loader必須要添加vue-loader-plugin
        .use(require("vue-loader").VueLoaderPlugin,[])
        .end()
    .plugin("html")// 添加html-webpack-plugin插件
        .use(require("html-webpack-plugin"),[{
            template: path.resolve(__dirname,"./public/index.html"), //指定模版文件
            chunks:["app"], //指定需要加載的chunk
            inject: "body" //指定script腳本注入的位置為body
        }])
        .end()
    .plugin("extract-css")//提取css樣式到單獨css文件
        .use(require('mini-css-extract-plugin'), [{
            filename: "css/[name].css",
            chunkFilename: "css/[name].css"
        }])
        .end()
    .devServer
        .host("0.0.0.0") //為了讓外部服務訪問
        .port(8090) //當前端口號
        .hot(true) //熱載
        .open(true) //開啟頁面
module.exports = config.toConfig();

然后我們在根目錄創建一個eslint配置文件,.eslintrc.json:

{
  "env": {
    "node": true //主要爭對webpack配置文件等等node環境
  },
  "plugins": [
    "vue" //添加eslint-plugin-vue插件
  ],
  "extends": [
    "eslint:recommended", //eslint推薦語法
    "plugin:vue/recommended", //使用vue推薦語法
    "@vue/typescript/recommended",//繼承typescript插件的recommended配置
    "@vue/prettier",
    "@vue/prettier/@typescript-eslint"
  ],
  "rules": {
    "semi": [
      "error",
      "always"
    ],
    "quotes": [
      "error",
      "double"
    ],
    "no-console": "error"
  }
}


eslint不熟的童鞋可以看前面關于eslint的文章。

有些文件我們不需要進行eslint校驗,所以我們直接在根目錄創建一個.eslintignore文件聲明,

.eslintignore:

node_modules/*
public/*
dist/*
webpack.config.js

然后我們運行npm run dev:

ERROR in ./src/app.tsx
Module Error (from ./node_modules/eslint-loader/dist/cjs.js):

/Users/ocj1/doc/h5/study/webpack/webpack-vue-demo/src/app.tsx
   1:9   warning  Replace `Vue,·Component` with `·Vue,·Component·`                                                                prettier/prettier
   5:3   warning  Delete `··`                                                                                                     prettier/prettier
   7:3   warning  Delete `··`                                                                                                     prettier/prettier
   7:5   warning  Missing return type on function                                                                                 @typescript-eslint/explicit-module-boundary-types
   7:12  warning  Argument 'h' should be typed                                                                                    @typescript-eslint/explicit-module-boundary-types
   7:12  warning  'h' is defined but never used                                                                                   @typescript-eslint/no-unused-vars
   8:1   warning  Replace `········return·(?············<div>{this.msg}</div>?········)` with `····return·<div>{this.msg}</div>`  prettier/prettier
  11:1   warning  Delete `··`                                                                                                     prettier/prettier
  12:2   warning  Insert `?`                                                                                                      prettier/prettier

? 9 problems (0 errors, 9 warnings)
  0 errors and 6 warnings potentially fixable with the `--fix` option.

 @ ./src/main.ts 3:0-24 7:13-16
 @ multi ./src/main.ts

ERROR in ./src/main.ts
Module Error (from ./node_modules/eslint-loader/dist/cjs.js):

/Users/ocj1/doc/h5/study/webpack/webpack-vue-demo/src/main.ts
  3:24  warning  Insert `;`              prettier/prettier
  3:24  error    Missing semicolon       semi
  7:11  warning  Replace `(h)` with `h`  prettier/prettier
  8:4   warning  Insert `?`              prettier/prettier

? 4 problems (1 error, 3 warnings)
  1 error and 3 warnings potentially fixable with the `--fix` option.

 @ multi ./src/main.ts app[0]
Child HtmlWebpackCompiler:
     1 asset
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
    [./node_modules/html-webpack-plugin/lib/loader.js!./public/index.html] 469 bytes {HtmlWebpackPlugin_0} [built]
? ?wdm?: Failed to compile.

可以看到,報了很多警告跟錯誤,我們嘗試修復一下,為了方便,我們在package.json中聲明一個lint腳本,

package.json:

{
  "name": "webpack-vue-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rimraf dist && webpack --mode=production",
    "dev": "webpack-dev-server --mode=development --progress",
    "lint": "eslint ./src/* --fix"
  },
  ...

然后我們執行npm run lint把一些能修復的修復掉:

...
Module Error (from ./node_modules/eslint-loader/dist/cjs.js):

xx/webpack/webpack-vue-demo/src/app.tsx
  7:3   warning  Missing return type on function  @typescript-eslint/explicit-module-boundary-types
  7:10  warning  Argument 'h' should be typed     @typescript-eslint/explicit-module-boundary-types
  7:10  warning  'h' is defined but never used    @typescript-eslint/no-unused-vars

? 3 problems (0 errors, 3 warnings)

可以看到有一些警告都是關于src/app.tsx文件的,

src/app.tsx:

import { Vue, Component } from "vue-property-decorator";

@Component
export default class AppTsx extends Vue {
  msg = "hello tsx";

  render(h) {
    return <div>{this.msg}</div>;
  }
}

說我們的render函數沒有定義返回值,ok! 我們直接加上返回值:

 import { VNode } from "vue";
import { Vue, Component } from "vue-property-decorator";

@Component
export default class AppTsx extends Vue {
  msg = "hello tsx";

  render(h):VNode {
    return <div>{this.msg}</div>;
  }
}

然后說h變量沒用,ok,我們直接去掉:

import { VNode } from "vue";
import { Vue, Component } from "vue-property-decorator";

@Component
export default class AppTsx extends Vue {
  msg = "hello tsx";

  render():VNode {
    return <div>{this.msg}</div>;
  }
}

ok,解決完畢后,我們再次執行npm run dev的時候就沒有錯誤跟警告信息了。

一般在開發環境為了更好的顯示錯誤信息,我們直接利用webpack.devServer的overlay顯示到頁面中去,

webpack.config.js:

const path = require("path");
const config = new (require("webpack-chain"))();
const isDev = !!process.env.WEBPACK_DEV_SERVER;
config
    .context(path.resolve(__dirname, ".")) //webpack上下文目錄為項目根目錄
    .entry("app") //入口文件名稱為app
        .add("./src/main.ts") //入口文件為./src/main.ts
        .end()
    .output
        .path(path.join(__dirname,"./dist")) //webpack輸出的目錄為根目錄的dist目錄
        .filename("[name].[hash:8].js")
        .end()
    .resolve
        .extensions
            .add(".js").add(".jsx").add(".ts").add(".tsx").add(".vue") //配置以.js等結尾的文件當模塊使用的時候都可以省略后綴
            .end()
        .end()
    .module
        .rule('js')
            .test(/\.m?jsx?$/) //對mjs、mjsx、js、jsx文件進行babel配置
            .exclude
                .add(filepath => {
                    // Don't transpile node_modules
                    return /node_modules/.test(filepath)
                })
                .end()
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .end()
        .rule("type-script")
            .test(/\.tsx?$/) //loader加載的條件是ts或tsx后綴的文件
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .use("ts-loader")
                .loader("ts-loader")
                .options({ //ts-loader相關配置
                    transpileOnly: true,
                    appendTsSuffixTo: ['\\.vue$']
                })
                .end()
            .end()
        .rule("vue")
            .test(/\.vue$/)// 匹配.vue文件
                .use("vue-loader")
                .loader("vue-loader")
                .end()
            .end()
        .rule("sass")
            .test( /\.(sass|scss)$/)//sass和scss文件
            .use("extract-loader")//提取css樣式到單獨css文件
                .loader(require('mini-css-extract-plugin').loader)
                .options({
                    hmr: isDev //開發環境開啟熱載
                })
                .end()
            .use("css-loader")//加載css模塊
                .loader("css-loader")
                .end()
            .use("postcss-loader")//處理css樣式
                .loader("postcss-loader")
                .options( {
                    config: {
                       path: path.resolve(__dirname, "./postcss.config.js")
                    }
                })
                .end()
            .use("sass-loader")//sass語法轉css語法
                .loader("sass-loader")
                .end()
            .end()
        .rule('eslint')//添加eslint-loader
            .exclude
                .add(/node_modules/)//校驗的文件除node_modules以外
                .end()
            .test(/\.(vue|(j|t)sx?)$/)//加載.vue、.js、.jsx、.ts、.tsx文件
                .use('eslint-loader')
                .loader(require.resolve('eslint-loader'))
                .options({
                    emitWarning: true, //把eslint報錯當成webpack警告
                    emitError: !isDev, //把eslint報錯當成webapck的錯誤
                })
                .end()
            .end()
         .end()
    .plugin("vue-loader-plugin")//vue-loader必須要添加vue-loader-plugin
        .use(require("vue-loader").VueLoaderPlugin,[])
        .end()
    .plugin("html")// 添加html-webpack-plugin插件
        .use(require("html-webpack-plugin"),[{
            template: path.resolve(__dirname,"./public/index.html"), //指定模版文件
            chunks:["app"], //指定需要加載的chunk
            inject: "body" //指定script腳本注入的位置為body
        }])
        .end()
    .plugin("extract-css")//提取css樣式到單獨css文件
        .use(require('mini-css-extract-plugin'), [{
            filename: "css/[name].css",
            chunkFilename: "css/[name].css"
        }])
        .end()
    .devServer
        .host("0.0.0.0") //為了讓外部服務訪問
        .port(8090) //當前端口號
        .hot(true) //熱載
        .open(true) //開啟頁面
        .overlay({
            warnings: true,
            errors: true
        }) //webpack錯誤和警告信息顯示到頁面
module.exports = config.toConfig();

fork-ts-checker-webpack-plugin

校驗ts語法,會把ts的一些報錯通過eslint展現出來。

yarn add -D fork-ts-checker-webpack-plugin || npm install -D fork-ts-checker-webpack-plugin

ok,然后我們把該插件配置到webpack:

babel.config.js

const path = require("path");
const config = new (require("webpack-chain"))();
const isDev = !!process.env.WEBPACK_DEV_SERVER;
config
    .context(path.resolve(__dirname, ".")) //webpack上下文目錄為項目根目錄
    .entry("app") //入口文件名稱為app
        .add("./src/main.ts") //入口文件為./src/main.ts
        .end()
    .output
        .path(path.join(__dirname,"./dist")) //webpack輸出的目錄為根目錄的dist目錄
        .filename("[name].[hash:8].js")
        .end()
    .resolve
        .extensions
            .add(".js").add(".jsx").add(".ts").add(".tsx").add(".vue") //配置以.js等結尾的文件當模塊使用的時候都可以省略后綴
            .end()
        .end()
    .module
        .rule('js')
            .test(/\.m?jsx?$/) //對mjs、mjsx、js、jsx文件進行babel配置
            .exclude
                .add(filepath => {
                    // Don't transpile node_modules
                    return /node_modules/.test(filepath)
                })
                .end()
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .end()
        .rule("type-script")
            .test(/\.tsx?$/) //loader加載的條件是ts或tsx后綴的文件
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .use("ts-loader")
                .loader("ts-loader")
                .options({ //ts-loader相關配置
                    transpileOnly: true,  // disable type checker - we will use it in fork plugin
                    appendTsSuffixTo: ['\\.vue$']
                })
                .end()
            .end()
        .rule("vue")
            .test(/\.vue$/)// 匹配.vue文件
                .use("vue-loader")
                .loader("vue-loader")
                .end()
            .end()
        .rule("sass")
            .test( /\.(sass|scss)$/)//sass和scss文件
            .use("extract-loader")//提取css樣式到單獨css文件
                .loader(require('mini-css-extract-plugin').loader)
                .options({
                    hmr: isDev //開發環境開啟熱載
                })
                .end()
            .use("css-loader")//加載css模塊
                .loader("css-loader")
                .end()
            .use("postcss-loader")//處理css樣式
                .loader("postcss-loader")
                .options( {
                    config: {
                       path: path.resolve(__dirname, "./postcss.config.js")
                    }
                })
                .end()
            .use("sass-loader")//sass語法轉css語法
                .loader("sass-loader")
                .end()
            .end()
        .rule('eslint')//添加eslint-loader
            .exclude
                .add(/node_modules/)//校驗的文件除node_modules以外
                .end()
            .test(/\.(vue|(j|t)sx?)$/)//加載.vue、.js、.jsx、.ts、.tsx文件
                .use('eslint-loader')
                .loader(require.resolve('eslint-loader'))
                .options({
                    emitWarning: true, //把eslint報錯當成webpack警告
                    emitError: !isDev, //把eslint報錯當成webapck的錯誤
                })
                .end()
            .end()
         .end()
    .plugin("vue-loader-plugin")//vue-loader必須要添加vue-loader-plugin
        .use(require("vue-loader").VueLoaderPlugin,[])
        .end()
    .plugin("html")// 添加html-webpack-plugin插件
        .use(require("html-webpack-plugin"),[{
            template: path.resolve(__dirname,"./public/index.html"), //指定模版文件
            chunks:["app"], //指定需要加載的chunk
            inject: "body" //指定script腳本注入的位置為body
        }])
        .end()
    .plugin("extract-css")//提取css樣式到單獨css文件
        .use(require('mini-css-extract-plugin'), [{
            filename: "css/[name].css",
            chunkFilename: "css/[name].css"
        }])
        .end()
    .plugin('fork-ts-checker') //配置fork-ts-checker
        .use(require('fork-ts-checker-webpack-plugin'), [{
            eslint: {
                files: './src/**/*.{ts,tsx,js,jsx,vue}' // required - same as command `eslint ./src/**/*.{ts,tsx,js,jsx} --ext .ts,.tsx,.js,.jsx`
            },
            typescript: {
                extensions: {
                    vue: {
                        enabled: true,
                        compiler: "vue-template-compiler"
                    },
                }
            }
        }])
        .end()
    .devServer
        .host("0.0.0.0") //為了讓外部服務訪問
        .port(8090) //當前端口號
        .hot(true) //熱載
        .open(true) //開啟頁面
        .overlay({
            warnings: true,
            errors: true
        }) //webpack錯誤和警告信息顯示到頁面
module.exports = config.toConfig();

ok, 配置完畢后我們簡單測試一下,比如我們的src/app.tsx:

import { VNode } from "vue";
import { Vue, Component } from "vue-property-decorator";

@Component
export default class AppTsx extends Vue {
  msg = "hello tsx";

  render(): VNode {
    return <div>{this.msg}</div>;
  }
}

然后我們在render方法中把msg的值改改:

import { VNode } from "vue";
import { Vue, Component } from "vue-property-decorator";

@Component
export default class AppTsx extends Vue {
  msg = "hello tsx";

  render(): VNode {
    this.msg = 1;
    return <div>{this.msg}</div>;
  }
}

我們直接把一個string類型的變量改成了number,然后我們運行npm run dev:

...
? ?wdm?: Compiled successfully.
Issues checking in progress...
ERROR in src/app.tsx 9:5-13
TS2322: Type '1' is not assignable to type 'string'.
     7 | 
     8 |   render(): VNode {
  >  9 |     this.msg = 1;
       |     ^^^^^^^^
    10 |     return <div>{this.msg}</div>;
    11 |   }
    12 | }
ERROR in src/app.tsx 9:5-13
TS2322: Type '1' is not assignable to type 'string'.
     7 | 
     8 |   render(): VNode {
  >  9 |     this.msg = 1;
       |     ^^^^^^^^
    10 |     return <div>{this.msg}</div>;
    11 |   }
    12 | }

ERROR in src/app.tsx 10:12-17
TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.
     8 |   render(): VNode {
     9 |     this.msg = 1;
  > 10 |     return <div>{this.msg}</div>;
       |            ^^^^^
    11 |   }
    12 | }
    13 | 

ERROR in src/app.tsx 10:27-33
TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.
     8 |   render(): VNode {
     9 |     this.msg = 1;
  > 10 |     return <div>{this.msg}</div>;
       |                           ^^^^^^
    11 |   }
    12 | }
    13 | 


ok, 可以看到,直接報錯了,說“Type '1' is not assignable to type 'string'.” 這說明我們的配置起作用了,同時還報了一些錯誤,說找不到“JSX.IntrinsicElements”類型。

ok! 我們直接在src目錄底下定義一個shims-tsx.d.ts文件,然后用命名空間形式去聲明一個JSX模塊,

shims-tsx.d.ts:

import Vue, { VNode } from "vue";

declare global {
  namespace JSX {
    // tslint:disable no-empty-interface
    interface Element extends VNode {}
    // tslint:disable no-empty-interface
    interface ElementClass extends Vue {}
    interface IntrinsicElements {
      [elem: string]: any;
    }
  }
}

再次運行會發現沒有報錯了。

optimization

生成環境的代碼壓縮并且去掉所有注釋

config.when(!isDev,()=>{
    config.optimization
            .minimize(true)
            .minimizer("terser")
            .use(require("terser-webpack-plugin"),[{
                extractComments: false, //去除注釋
                terserOptions:{
                    output: {
                        comments: false //去除注釋
                    }
                }
            }]);
},()=>{

});

devtool開發改成“eval-cheap-module-source-map”加快速度

config.when(!isDev,()=>{
    config.optimization
            .minimize(true)
            .minimizer("terser")
            .use(require("terser-webpack-plugin"),[{
                extractComments: false, //去除注釋
                terserOptions:{
                    output: {
                        comments: false //去除注釋
                    }
                }
            }]);
},()=>{
    config.devtool("eval-cheap-module-source-map");
});

分包機制優化

config.optimization
            .splitChunks({
                cacheGroups: {
                    vendors: { //分離入口文件引用node_modules的module(vue、@babel/xxx)
                        name: `chunk-vendors`,
                        test: /[\\/]node_modules[\\/]/,
                        priority: -10,
                        chunks: 'initial'
                    },
                    common: { //分離入口文件引用次數>=2的module
                        name: `chunk-common`,
                        minChunks: 2,
                        priority: -20,
                        chunks: 'initial',
                        reuseExistingChunk: true
                    }
                }
            })
            .runtimeChunk("single"); //分離webpack的一些幫助函數,比如webpackJSONP等等

同樣,html-webpack-plugin插件也需要修改一下:

 .plugin("html")// 添加html-webpack-plugin插件
        .use(require("html-webpack-plugin"),[{
            template: path.resolve(__dirname,"./public/index.html"), //指定模版文件
            chunks: ["runtime", "chunk-vendors", "chunk-common", "app"], //指定需要加載的chunks
            inject: "body" //指定script腳本注入的位置為body
        }])
        .end()

webpack全局配置,webpack.config.js:

const path = require("path");
const config = new (require("webpack-chain"))();
const isDev = !!process.env.WEBPACK_DEV_SERVER;
config
    .context(path.resolve(__dirname, ".")) //webpack上下文目錄為項目根目錄
    .entry("app") //入口文件名稱為app
        .add("./src/main.ts") //入口文件為./src/main.ts
        .end()
    .output
        .path(path.join(__dirname,"./dist")) //webpack輸出的目錄為根目錄的dist目錄
        .filename("[name].[hash:8].js")
        .end()
    .resolve
        .extensions
            .add(".js").add(".jsx").add(".ts").add(".tsx").add(".vue") //配置以.js等結尾的文件當模塊使用的時候都可以省略后綴
            .end()
        .end()
    .module
        .rule('js')
            .test(/\.m?jsx?$/) //對mjs、mjsx、js、jsx文件進行babel配置
            .exclude
                .add(filepath => {
                    // Don't transpile node_modules
                    return /node_modules/.test(filepath)
                })
                .end()
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .end()
        .rule("type-script")
            .test(/\.tsx?$/) //loader加載的條件是ts或tsx后綴的文件
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .use("ts-loader")
                .loader("ts-loader")
                .options({ //ts-loader相關配置
                    transpileOnly: true,  // disable type checker - we will use it in fork plugin
                    appendTsSuffixTo: ['\\.vue$']
                })
                .end()
            .end()
        .rule("vue")
            .test(/\.vue$/)// 匹配.vue文件
                .use("vue-loader")
                .loader("vue-loader")
                .end()
            .end()
        .rule("sass")
            .test( /\.(sass|scss)$/)//sass和scss文件
            .use("extract-loader")//提取css樣式到單獨css文件
                .loader(require('mini-css-extract-plugin').loader)
                .options({
                    hmr: isDev //開發環境開啟熱載
                })
                .end()
            .use("css-loader")//加載css模塊
                .loader("css-loader")
                .end()
            .use("postcss-loader")//處理css樣式
                .loader("postcss-loader")
                .options( {
                    config: {
                       path: path.resolve(__dirname, "./postcss.config.js")
                    }
                })
                .end()
            .use("sass-loader")//sass語法轉css語法
                .loader("sass-loader")
                .end()
            .end()
        .rule('eslint')//添加eslint-loader
            .exclude
                .add(/node_modules/)//校驗的文件除node_modules以外
                .end()
            .test(/\.(vue|(j|t)sx?)$/)//加載.vue、.js、.jsx、.ts、.tsx文件
                .use('eslint-loader')
                .loader(require.resolve('eslint-loader'))
                .options({
                    emitWarning: true, //把eslint報錯當成webpack警告
                    emitError: !isDev, //把eslint報錯當成webapck的錯誤
                })
                .end()
            .end()
         .end()
    .plugin("vue-loader-plugin")//vue-loader必須要添加vue-loader-plugin
        .use(require("vue-loader").VueLoaderPlugin,[])
        .end()
    .plugin("html")// 添加html-webpack-plugin插件
        .use(require("html-webpack-plugin"),[{
            template: path.resolve(__dirname,"./public/index.html"), //指定模版文件
            chunks: ["runtime", "chunk-vendors", "chunk-common", "app"], //指定需要加載的chunks
            inject: "body" //指定script腳本注入的位置為body
        }])
        .end()
    .plugin("extract-css")//提取css樣式到單獨css文件
        .use(require('mini-css-extract-plugin'), [{
            filename: "css/[name].css",
            chunkFilename: "css/[name].css"
        }])
        .end()
    .plugin('fork-ts-checker') //配置fork-ts-checker
        .use(require('fork-ts-checker-webpack-plugin'), [{
            eslint: {
                files: './src/**/*.{ts,tsx,js,jsx,vue}' // required - same as command `eslint ./src/**/*.{ts,tsx,js,jsx} --ext .ts,.tsx,.js,.jsx`
            },
            typescript: {
                extensions: {
                    vue: {
                        enabled: true,
                        compiler: "vue-template-compiler"
                    },
                }
            }
        }])
        .end()
    .devServer
        .host("0.0.0.0") //為了讓外部服務訪問
        .port(8090) //當前端口號
        .hot(true) //熱載
        .open(true) //開啟頁面
        .overlay({
            warnings: true,
            errors: true
        }) //webpack錯誤和警告信息顯示到頁面
config.when(!isDev,()=>{
    config.optimization
            .minimize(true)
            .minimizer("terser")
            .use(require("terser-webpack-plugin"),[{
                extractComments: false, //去除注釋
                terserOptions:{
                    output: {
                        comments: false //去除注釋
                    }
                }
            }]);
},()=>{
    config.devtool("eval-cheap-module-source-map");
});
config.optimization
            .splitChunks({
                cacheGroups: {
                    vendors: { //分離入口文件引用node_modules的module(vue、@babel/xxx)
                        name: `chunk-vendors`,
                        test: /[\\/]node_modules[\\/]/,
                        priority: -10,
                        chunks: 'initial'
                    },
                    common: { //分離入口文件引用次數>=2的module
                        name: `chunk-common`,
                        minChunks: 2,
                        priority: -20,
                        chunks: 'initial',
                        reuseExistingChunk: true
                    }
                }
            })
            .runtimeChunk("single"); //分離webpack的一些幫助函數,比如webpackJSONP等等

module.exports = config.toConfig();

總結

ok,到這我們的vue實戰算是結束了,大家也可以直接把demo用到項目中,沒毛病!!我們花了兩節來實戰了一個vue項目,有小伙伴有疑問了:“這么多內容我們怎么記得住呢?” 是的! 那么多知識點我也是記不住的,但是這個是真沒辦法的,你需要時刻關注一些更新動態或者社區大佬分享的一些經驗了,不過說到底,只有你自己明白你需要的是什么就夠了,不要自己都懵逼了那就完了,建議初級剛接觸前端的童鞋可以直接用官方的腳手架生成項目,但是對于中級和高級的同學那就需要自己掌握每一個知識點了,要有脫離腳手架也能擼一個項目的功底!!

demo鏈接地址:https://github.com/913453448/webpack-vue-demo.git

好啦~~ 本節到此就結束了,未完待續!!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。