從零開始構建 Vue + TypeScript 腳手架模版

前言

一般咱們開發vue項目,都會使用到vue-cli腳手架工具構建想要的項目模版,這樣當然能無視各類配置,高效地編寫項目。但是vue-cli這樣相似黑盒的模版配置,對於初學者來講,實際上是很傷的。天天都只會關注業務邏輯代碼,寫了一兩年甚至連webpack的基本配置都不瞭解。javascript

並且,vue-cli 即便方便,也不能徹底契合咱們的項目,這時咱們就須要手動的構建項目初始模版。爲實現項目的工程化打下基礎。css

項目地址

項目地址html

  • master: webpack4 + express + vue
  • ts-dev: webpack4 + express + vue(vue-class-component) + typescript (本文講解)

構建文件目錄

`-- build                                   構建文件目錄
|   |-- configs                             項目配置
|       |-- appEnvs.js                      全局變量配置
|       |-- options.js                      其餘配置
|       |-- proxy.js                        服務代理配置
|   |-- plugin                              插件
|   |-- rules                               規則
|   `-- development.js                      開發環境配置
|   `-- production.js                       生產環境配置
|   `-- webpack.base.js                     基礎環境配置
|-- public                                  公共文件夾
`-- src                                     代碼目錄
    |-- @types                              typescript 聲明文件
    |-- http                                http 文件
    |-- assets                              多媒體 文件
    |-- components                          組件 文件
    |-- store                               狀態 文件
    |-- utils                               工具 文件
    |-- views                               視圖 文件夾 
    |   |-- home
    |        |-- home.module.scss           css module 文件
    |        |-- index.tsx                  tsx 文件
    |-- App.tsx        
    |-- main.ts                             入口文件   
    |-- router.ts                           路由文件   
|-- .editorconfig
|-- .prettierrc         
|-- .postcssrc.js
|-- babel.config.js
|-- package.json
|-- tsconfig.json
|-- tslint.json
複製代碼

webpack 配置

  • webpack.base.js
const path = require("path");
// 拋出一些配置, 好比port, builtPath
const config = require("./configs/options");
// css less scss loder 整合
const cssLoaders = require("./rules/cssLoaders");

function resolve(name) {
  return path.resolve(__dirname, "..", name);
}

// 開發環境變動不刷新頁面,熱替換
function addDevClient(options) {
  if (options.mode === "development") {
    Object.keys(options.entry).forEach(name => {
      options.entry[name] = [
        "webpack-hot-middleware/client?reload=true&noInfo=true"
      ].concat(options.entry[name]);
    });
  }
  return options.entry;
}

// webpack 配置
module.exports = options => {
  const entry = addDevClient({
    entry: {
      app: [resolve("src/main.ts")]
    },
    mode: options.mode
  });
  return {
    // Webpack打包的入口
    entry: entry,
    // 定義webpack如何輸出的選項
    output: {
      publicPath: "/",  // 構建文件的輸出目錄
      path: resolve(config.builtPath || "dist"), // 全部輸出文件的目標路徑
      filename: "static/js/[name].[hash].js", // 「入口(entry chunk)」文件命名模版
      chunkFilename: "static/js/[name].[chunkhash].js" // 非入口(non-entry) chunk 文件的名稱
    },
    resolve: {
      // 模塊的查找目錄
      modules: [resolve("node_modules"), resolve("src")],
      // 用到的文件的擴展
      extensions: [".tsx", ".ts", ".js", ".vue", ".json"],
      // 模塊別名列表
      alias: {
        vue$: "vue/dist/vue.esm.js",
        "@components": resolve("src/components"),
        "@": resolve("src")
      }
    },
    // 防止將某些 import 的包(package)打包到 bundle 中,
    // 而是在運行時(runtime)再去從外部獲取這些擴展依賴(external dependencies)。
    // 減小打包後的問價體積。在首頁加入 cdn 引入
    externals: {
      vue: "Vue",
      vuex: "Vuex",
      "vue-router": "VueRouter"
    },
    // 模塊相關配置
    module: {
      rules: [
        {
          test: /(\.jsx|\.js)$/,
          use: ["babel-loader"],
          exclude: /node_modules/
        },
        // .tsx 文件的解析
        {
          test: /(\.tsx)$/,
          exclude: /node_modules/,
          use: ["babel-loader", "vue-jsx-hot-loader", "ts-loader"]
        },
        {
          test: /(\.ts)$/,
          exclude: /node_modules/,
          use: ["babel-loader", "ts-loader"]
        },
        ...cssLoaders({
          mode: options.mode,
          sourceMap: options.sourceMap,
          extract: options.mode === "production"
        }),
        {
          test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
          loader: "url-loader",
          options: {
            limit: 10000,
            name: "static/img/[name].[hash:7].[ext]"
          }
        },
        {
          test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
          loader: "url-loader",
          options: {
            limit: 10000,
            name: "static/media/[name].[hash:7].[ext]",
            fallback: "file-loader"
          }
        },
        {
          test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
          loader: "url-loader",
          options: {
            limit: 10000,
            name: "static/fonts/[name].[hash:7].[ext]"
          }
        }
      ]
    }
  };
};

複製代碼

開發環境配置vue

  • development.js
const webpack = require('webpack')
const path = require('path')

const express = require('express')

const merge = require('webpack-merge')
const chalk = require('chalk')

// 兩個合體實現本地服務熱替換
// 具體實現 https://github.com/webpack-contrib/webpack-hot-middleware
const webpackMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')

const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')

const baseConfig = require('./webpack.base')
const config = require('./configs/options')

const HtmlWebpackPlugin = require('html-webpack-plugin')

const proxyTable = require('./configs/proxy')

// http-proxy-middleware 添加代理
const useExpressProxy = require('./plugins/useExpressProxy')

// 全局變量
const appEnvs = require('./configs/appEnvs')
const app = express()

// 合併 webpack 請求
const compiler = webpack(merge(baseConfig({ mode: 'development' }), {
  mode: 'development',
  devtool: '#cheap-module-eval-source-map',
  // 插件
  plugins: [
    new ProgressBarPlugin(), // 進度條插件
    new FriendlyErrorsWebpackPlugin(),
    // 經過 DefinePlugin 來設置 process.env 環境變量的快捷方式。
    new webpack.EnvironmentPlugin(appEnvs),
    // 模塊熱替換插件, 與 webpack-hot-middleware 配套使用
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      template: resolve('./public/index.html'),
      filename: 'index.html'
    })
  ],
  optimization: {
    // 跳過生成階段,不會由於錯誤代碼退出
    noEmitOnErrors: true
  }
}))

function resolve (name) {
  return path.resolve(__dirname, '..', name)
}

const devMiddleware = webpackMiddleware(compiler, {
  // 同 webpack publicPath
  publicPath: '/',
  logLevel: 'silent'
})

const hotMiddleware = webpackHotMiddleware(compiler, {
  log: false
})

compiler.hooks.compilation.tap('html-webpack-plugin-after-emit', () => {
  hotMiddleware.publish({
    action: 'reload'
  })
})

// 加載中間件
app.use(devMiddleware)
app.use(hotMiddleware)

// 添加代理配置
useExpressProxy(app, proxyTable)

devMiddleware.waitUntilValid(() => {
  console.log(chalk.yellow(`I am ready. open http://localhost:${ config.port || 3000 } to see me.`))
})

app.listen(config.port || 3000)

複製代碼

生產環境配置java

  • production.js
const webpack = require('webpack')
const path = require('path')

const ora = require('ora')
const chalk = require('chalk')
const merge = require('webpack-merge')

const baseConfig = require('./webpack.base.js')

// 替代 extract-text-webpack-plugin, 用於提取 css 文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 用於 css 文件優化壓縮
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')

const HtmlWebpackPlugin = require('html-webpack-plugin')
// 從新構建時清空 dist 文件
const CleanWebpackPlugin = require('clean-webpack-plugin')

const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

const config = require('./configs/options')
const appEnvs = require('./configs/appEnvs')

const compiler = webpack(merge(baseConfig({ mode: 'production' }), {
  mode: 'production',
  output: {
    publicPath: './'
  },
  performance: {
    hints: false
  },
  plugins: [
    new webpack.HashedModuleIdsPlugin(),
    new webpack.EnvironmentPlugin(appEnvs),
    new webpack.SourceMapDevToolPlugin({
      test: /\.js$/,
      filename: 'sourcemap/[name].[chunkhash].map',
      append: false
    }),
    new CleanWebpackPlugin([`${config.builtPath || 'dist'}/*`], {
      root: path.resolve(__dirname, '..')
    }),
    new HtmlWebpackPlugin({
      template: resolve('./public/index.html'),
      filename: 'index.html',
      chunks: ['app', 'vendors', 'mainifest'],
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
      }
    }),
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash].css'
      chunkFilename: 'static/css/[id].[contenthash].css'
    })
  ],
  optimization: {
    // 將webpack運行時生成代碼打包到 mainifest.js
    runtimeChunk: {
      name: 'mainifest'
    },
    // 替代 commonChunkPlugin, 拆分代碼
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        // node_modules 中用到的合併到 vendor.js
        vendor: {
          test: /node_modules\/(.*)\.js/,
          name: 'vendors',
          chunks: 'initial',
          priority: -10,
          reuseExistingChunk: false
        },
        // 與 mini-css-extract-plugin 配合,將 css 整合到一個文件
        styles: {
          name: 'styles',
          test:  /(\.less|\.scss|\.css)$/,
          chunks: 'all',
          enforce: true,
        },
      }
    },
    minimizer: [
      // ParallelUglifyPlugin 能夠把對JS文件的串行壓縮變爲開啓多個子進程並行執行
      new ParallelUglifyPlugin({
        uglifyJS: {
          output: {
            beautify: false,
            comments: false
          },
          compress: {
            warnings: false,
            drop_console: true,
            collapse_vars: true,
            reduce_vars: true
          }
        },
        cache: true, // 開啓緩存
        parallel: true, // 平行壓縮
        sourceMap: true // set to true if you want JS source maps
      }),
      // 壓縮 css
      new OptimizeCssAssetsPlugin({
        assetNameRegExp: /(\.less|\.scss|\.css)$/g,
        cssProcessor: require("cssnano"), // css 壓縮優化器
          cssProcessorOptions: {
            safe: true,
            autoprefixer: { disable: true },
            discardComments: { removeAll: true }
          }, // 去除全部註釋
        canPrint: true
      })
    ]
  }
}))

function resolve (name) {
  return path.resolve(__dirname, '..', name)
}

const spinner = ora('building for production...').start()

compiler.run((err, stats) => {
  spinner.stop()
  if (err) throw err
  process.stdout.write(stats.toString({
    colors: true,
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false
  }) + '\n\n')

  console.log(chalk.cyan(' Build complete..\n'))
  console.log(chalk.yellow(
    ' Tip: built files are meant to be served over an HTTP server.\n' +
    ' Opening index.html over file:// won\'t work.\n'
  ))
})
複製代碼

Vue + TypeScript

爲了在 vue 中使用 typescript, 咱們使用的是 vue-class-component, 首先咱們須要讓項目兼容 jsx 以及 typescriptnode

Babel 7

我使用的是 babel@7, 相比較 6, 有些許改動,全部的packages@babel/xxxwebpack

對於 jsx 的兼容,我直接使用了 @vue/babel-preset-jsx, 內部加載了 babel-plugin-transform-jsxgit

  • babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        modules: false,
        targets: {
          browsers: ['> 1%', 'last 2 versions', 'not ie <= 8']
        }
      }
    ],
    '@vue/babel-preset-jsx'
  ],
  plugins: [
    '@babel/plugin-transform-runtime'
  ],
  comments: false,
  env: {
    test: {
      presets: ['@babel/preset-env'],
      plugins: ['babel-plugin-dynamic-import-node']
    }
  }
}
複製代碼

TsConfig

  • tsconfig.js
{
  "include": [
      "src/**/*.ts",
      "src/**/*.tsx",
      "src/**/*.vue",
      "tests/**/*.ts",
      "tests/**/*.tsx"
  ],
  "exclude": ["node_modules"],
  "compilerOptions": {
      // typeRoots option has been previously configured
      "typeRoots": [
          // add path to @types
          "src/@types"
      ],
      "baseUrl": ".",
      "paths": {
          "*": ["types/*"],
          "@/*": ["src/*"]
      },
      // 以嚴格模式解析
      "strict": true,
      // 在.tsx文件裏支持JSX
      "jsx": "preserve",
      // 使用的JSX工廠函數
      "jsxFactory": "h",
      // 容許從沒有設置默認導出的模塊中默認導入
      "allowSyntheticDefaultImports": true,
      // 啓用裝飾器
      "experimentalDecorators": true,
      // "strictFunctionTypes": false,
      // 容許編譯javascript文件
      "allowJs": true,
      // 採用的模塊系統
      "module": "esnext",
      // 編譯輸出目標 ES 版本
      "target": "es5",
      // 如何處理模塊
      "moduleResolution": "node",
      // 在表達式和聲明上有隱含的any類型時報錯
      "noImplicitAny": true,
      "importHelpers": true,
      "lib": ["dom", "es5", "es6", "es7", "es2015.promise"],
      "sourceMap": true,
      "pretty": true,
      "esModuleInterop": true
  }
}

複製代碼
  • tslint.js
{
  "defaultSeverity": "warning",
  "extends": ["tslint:recommended"],
  "linterOptions": {
    "exclude": ["node_modules/**"]
  },
  "allowJs": true,
  "rules": {
    "arrow-parens": false,
    "trailing-comma": false,
    "quotemark": [true],
    "indent": [true, "spaces", 2],
    "interface-name": false,
    "ordered-imports": false,
    "object-literal-sort-keys": false,
    "no-console": false,
    "no-debugger": false,
    "no-unused-expression": [true, "allow-fast-null-checks"],
    "no-unused-variable": false,
    "triple-equals": true,
    "no-parameter-reassignment": true,
    "no-conditional-assignment": true,
    "no-construct": true,
    "no-duplicate-super": true,
    "no-duplicate-switch-case": true,
    "no-object-literal-type-assertion": true,
    "no-return-await": true,
    "no-sparse-arrays": true,
    "no-string-throw": true,
    "no-switch-case-fall-through": true,
    "prefer-object-spread": true,
    "radix": true,
    "cyclomatic-complexity": [true, 20],
    "member-access": false,
    "deprecation": false,
    "use-isnan": true,
    "no-duplicate-imports": true,
    "no-mergeable-namespace": true,
    "encoding": true,
    "import-spacing": true,
    "interface-over-type-literal": true,
    "new-parens": true,
    "no-angle-bracket-type-assertion": true,
    "no-consecutive-blank-lines": [true, 3]
  }
}
複製代碼

項目代碼

萬事俱備以後,咱們開始編寫 .tsx 文件es6

  • App.tsx
import { Vue, Component } from "vue-property-decorator";
import { CreateElement } from "vue";

@Component
export default class extends Vue {
  // 這裏 h: CreateElement 是重點,沒有就會報錯
  // 沒有自動注入h, 涉及到 babel-plugin-transform-jsx 的問題
  // 本身也不明白,爲何會沒有自動注入
  render(h: CreateElement) {
    return (
      <div id="app"> <router-view /> </div> ); } } 複製代碼

結尾

文章沒有寫的很細,其實不少重要的項目配置尚未加上,好比 commitizen, lint-stage, jest, cypress, babel-plugin-vue-jsx-sync, babel-plugin-jsx-v-model.....github

更多的也是爲了從零開始瞭解體會整個項目的構建過程。

代碼已上傳。歡迎來訪。 項目地址

相關文章
相關標籤/搜索