簡單vue專案腳手架

xiaobinwu發表於2019-02-16

簡單vue專案腳手架

github地址

使用技術棧

  • webpack(^2.6.1)

  • webpack-dev-server(^2.4.5)

  • vue(^2.3.3)

  • vuex(^2.3.1)

  • vue-router(^2.5.3)

  • vue-loader(^12.2.1)

  • eslint(^3.19.0)

需要學習的知識

vue.js
vuex
vue-router
vue-loader
webpack2
eslint
內容相當多,尤其是webpack2教程,官方腳手架vue-cli雖然相當完整齊全,但是修改起來還是挺花時間,於是自己參照網上的資料和之前做過的專案用到的構建工具地去寫了一個簡單vue專案腳手架。適用於多頁面spa模式的業務場景(每個模組都是一個spa)。比較簡單,主要就是一個webpack.config.js檔案,沒有說特意地去劃分成分webpack.dev.config.js、webpack.prov.config.js等等。下面是整個webpack.config.js檔案程式碼:

const { resolve } = require(`path`)
const webpack = require(`webpack`)
const HtmlWebpackPlugin = require(`html-webpack-plugin`)
const ExtractTextPlugin = require(`extract-text-webpack-plugin`)
const glob = require(`glob`)

module.exports = (options = {}) => {
    // 配置檔案,根據 run script不同的config引數來呼叫不同config
    const config = require(`./config/` + (process.env.npm_config_config || options.config || `dev`))
    // 遍歷入口檔案,這裡入口檔案與模板檔名字保持一致,保證能同時合成HtmlWebpackPlugin陣列和入口檔案陣列
    const entries = glob.sync(`./src/modules/*.js`)
    const entryJsList = {}
    const entryHtmlList = []
    for (const path of entries) {
        const chunkName = path.slice(`./src/modules/`.length, -`.js`.length)
        entryJsList[chunkName] = path
        entryHtmlList.push(new HtmlWebpackPlugin({
            template: path.replace(`.js`, `.html`),
            filename: `modules/` + chunkName + `.html`,
            chunks: [`manifest`, `vendor`, chunkName]
        }))
    }
    // 處理開發環境和生產環境ExtractTextPlugin的使用情況
    function cssLoaders(loader, opt) {
        const loaders = loader.split(`!`)
        const opts = opt || {}
        if (options.dev) {
            if (opts.extract) {
                return loader
            } else {
                return loaders
            }
        } else {
            const fallbackLoader = loaders.shift()
            return ExtractTextPlugin.extract({
                use: loaders,
                fallback: fallbackLoader
            })
        }
    }

    const webpackObj = {
        entry: Object.assign({
            vendor: [`vue`, `vuex`, `vue-router`]
        }, entryJsList),
        // 檔案內容生成雜湊值chunkhash,使用hash會更新所有檔案
        output: {
            path: resolve(__dirname, `dist`),
            filename: options.dev ? `static/js/[name].js` : `static/js/[name].[chunkhash].js`,
            chunkFilename: `static/js/[id].[chunkhash].js`,
            publicPath: config.publicPath
        },

        externals: {

        },

        module: {
            rules: [
                // 只 lint 本地 *.vue 檔案,需要安裝eslint-plugin-html,並配置eslintConfig(package.json)
                {
                    enforce: `pre`,
                    test: /.vue$/,
                    loader: `eslint-loader`,
                    exclude: /node_modules/
                },
                /*
                    http://blog.guowenfh.com/2016/08/07/ESLint-Rules/
                    http://eslint.cn/docs/user-guide/configuring
                    [eslint資料]
                 */
                {
                    test: /.js$/,
                    exclude: /node_modules/,
                    use: [`babel-loader`, `eslint-loader`]
                },
                // 需要安裝vue-template-compiler,不然編譯報錯
                {
                    test: /.vue$/,
                    loader: `vue-loader`,
                    options: {
                        loaders: {
                            sass: cssLoaders(`vue-style-loader!css-loader!sass-loader`, { extract: true })
                        }
                    }
                },
                {
                    // 需要有相應的css-loader,因為第三方庫可能會有檔案
                    // (如:element-ui) css在node_moudle
                    // 生產環境才需要code抽離,不然的話,會使熱過載失效
                    test: /.css$/,
                    use: cssLoaders(`style-loader!css-loader`)
                },
                {
                    test: /.(scss|sass)$/,
                    use: cssLoaders(`style-loader!css-loader!sass-loader`)
                },
                {
                    test: /.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(?.+)?$/,
                    use: [
                        {
                            loader: `url-loader`,
                            options: {
                                limit: 10000,
                                name: `static/imgs/[name].[ext]?[hash]`
                            }
                        }
                    ]
                }
            ]
        },

        plugins: [
            ...entryHtmlList,
            // 抽離css
            new ExtractTextPlugin({
                filename: `static/css/[name].[chunkhash].css`,
                allChunks: true
            }),
            // 抽離公共程式碼
            new webpack.optimize.CommonsChunkPlugin({
                names: [`vendor`, `manifest`]
            }),
            // 定義全域性常量
            // cli命令列使用process.env.NODE_ENV不如期望效果,使用不了,所以需要使用DefinePlugin外掛定義,定義形式`"development"`或JSON.stringify(`development`)
            new webpack.DefinePlugin({
                `process.env`: {
                    NODE_ENV: options.dev ? JSON.stringify(`development`) : JSON.stringify(`production`)
                }
            })

        ],

        resolve: {
            // require時省略的副檔名,不再需要強制轉入一個空字串,如:require(`module`) 不需要module.js
            extensions: [`.js`, `.json`, `.vue`, `.scss`, `.css`],
            // require路徑簡化
            alias: {
                `~`: resolve(__dirname, `src`),
                // Vue 最早會打包生成三個檔案,一個是 runtime only 的檔案 vue.common.js,一個是 compiler only 的檔案 compiler.js,一個是 runtime + compiler 的檔案 vue.js。
                // vue.js = vue.common.js + compiler.js,預設package.json的main是指向vue.common.js,而template 屬性的使用一定要用compiler.js,因此需要在alias改變vue指向
                vue: `vue/dist/vue`
            },
            // 指定import從哪個目錄開始查詢
            modules: [
                resolve(__dirname, `src`),
                `node_modules`
            ]
        },
        // 開啟http服務,publicPath => 需要與Output保持一致 || proxy => 反向代理 || port => 埠號
        devServer: config.devServer ? {
            port: config.devServer.port,
            proxy: config.devServer.proxy,
            publicPath: config.publicPath,
            stats: { colors: true }
        } : undefined,
        // 遮蔽檔案超過限制大小的warn
        performance: {
            hints: options.dev ? false : `warning`
        },
        // 生成devtool,保證在瀏覽器可以看到原始碼,生產環境設為false
        devtool: `inline-source-map`
    }

    if (!options.dev) {
        webpackObj.devtool = false
        webpackObj.plugins = (webpackObj.plugins || []).concat([
            // 壓縮js
            new webpack.optimize.UglifyJsPlugin({
                // webpack2,預設為true,可以不用設定
                compress: {
                    warnings: false
                }
            }),
            //  壓縮 loaders
            new webpack.LoaderOptionsPlugin({
                minimize: true
            })
        ])
    }

    return webpackObj
}

上面的程式碼對於每個配置項都有註釋說明,這裡有幾點需要注意的:

1. webpack.config.js匯出的是一個function

之前專案的webpack.config.js是以物件形式export的,如下

module.exports = {
    entry: ...,
    output: {
        ...
    },
    ...
}

而現在倒出來的是一個function,如下:

module.exports = (options = {}) => { 
    return {
        entry: ...,
        output: {
            ...
        },
        ...
    }
}

這樣的話,function會在執行webpack CLI的時候獲取webpack的引數,通過options傳進function,看一下package.json:

    "local": "npm run dev --config=local",
    "dev": "webpack-dev-server -d --hot --inline --env.dev --env.config dev",
    "build": "rimraf dist && webpack -p --env.config prod" //rimraf清空dist目錄

對於local命令,我們執行的是dev命令,但是在最後面會--config=local,這是配置,這樣我們可以通過process.env.npm_config_config獲取到,而對於dev命令,對於--env XXX,我們便可以在function獲取option.config= `dev` 和 option.dev= true的值,特別方便!以此便可以同步引數來載入不同的配置檔案了。對於-d-p不清楚的話,可以這裡檢視,很詳細!

    // 配置檔案,根據 run script不同的config引數來呼叫不同config
    const config = require(`./config/` + (process.env.npm_config_config || options.config || `dev`))

2. modules放置模板檔案、入口檔案、對應模組的vue檔案

將入口檔案和模板檔案放到modules目錄(名字保持一致),webpack檔案會通過glob讀取modules目錄,遍歷生成入口檔案物件和模板檔案陣列,如下:

    const entries = glob.sync(`./src/modules/*.js`)
    const entryJsList = {}
    const entryHtmlList = []
    for (const path of entries) {
        const chunkName = path.slice(`./src/modules/`.length, -`.js`.length)
        entryJsList[chunkName] = path
        entryHtmlList.push(new HtmlWebpackPlugin({
            template: path.replace(`.js`, `.html`),
            filename: `modules/` + chunkName + `.html`,
            chunks: [`manifest`, `vendor`, chunkName]
        }))
    }

對於HtmlWebpackPlugin外掛中幾個配置項的意思是,template:模板路徑,filename:檔名稱,這裡為了區分開來模板檔案我是放置在dist/modules資料夾中,而對應的編譯打包好的js、img(對於圖片我們是使用file-loader、url-loader進行抽離,對於這兩個不是很理解的,可以看這裡)、css我也是會放在dist/下對應目錄的,這樣目錄會比較清晰。chunks:指定插入檔案中的chunk,後面我們會生成manifest檔案、公共vendor、以及對應生成的jscss(名稱一樣)

3. 處理開發環境和生產環境ExtractTextPlugin的使用情況

開發環境,不需要把css進行抽離,要以style插入html檔案中,可以很好實現熱替換
生產環境,需要把css進行抽離合並,如下(根據options.dev區分開發和生產):

    // 處理開發環境和生產環境ExtractTextPlugin的使用情況
    function cssLoaders(loader, opt) {
        const loaders = loader.split(`!`)
        const opts = opt || {}
        if (options.dev) {
            if (opts.extract) {
                return loader
            } else {
                return loaders
            }
        } else {
            const fallbackLoader = loaders.shift()
            return ExtractTextPlugin.extract({
                use: loaders,
                fallback: fallbackLoader
            })
        }
    }
    ...
    // 使用情況
    // 注意:需要安裝vue-template-compiler,不然編譯會報錯
    {
        test: /.vue$/,
        loader: `vue-loader`,
        options: {
            loaders: {
                sass: cssLoaders(`vue-style-loader!css-loader!sass-loader`, { extract: true })
            }
        }
    },
    ...
    {
        test: /.(scss|sass)$/,
        use: cssLoaders(`style-loader!css-loader!sass-loader`)
    }

再使用ExtractTextPlugin合併抽離到static/css/目錄

4. 定義全域性常量

cli命令列(webpack -p)使用process.env.NODE_ENV不如期望效果,使用不了,所以需要使用DefinePlugin外掛定義,定義形式`”development”`或JSON.stringify(process.env.NODE_ENV),我使用這樣的寫法`development`,結果報錯(針對webpack2),查詢了一下網上資料,是這樣講的,可以去看一下,設定如下:

    new webpack.DefinePlugin({
        `process.env`: {
            NODE_ENV: options.dev ? JSON.stringify(`development`) : JSON.stringify(`production`)
        }
    })

5. 使用eslint修正程式碼規範

通過eslint來檢查程式碼的規範性,通過定義一套配置項,來規範程式碼,這樣多人協作,寫出來的程式碼也會比較優雅,不好的地方是,就是配置項太多,有些預設項設定我們不需要,但是確是處處限制我們,需要通過配置遮蔽掉,可以通過.eslintrc 檔案或是package.json的eslintConfig,還有其他方式,可以到中文網看,這裡我用的是package.json方式,如下:

    ...
  "eslintConfig": {
    "parser": "babel-eslint",
    "extends": "enough",
    "env": {
      "browser": true,
      "node": true,
      "commonjs": true,
      "es6": true
    },
    "rules": {
      "linebreak-style": 0,
      "indent": [2, 4],
      "no-unused-vars": 0,
      "no-console": 0
    },
    "plugins": [
      "html"
    ]
  },
  ...

我們還需要安裝 npm install eslint eslint-config-enough eslint-loader --save-dev,eslint-config-enough是所謂的配置檔案,這樣package.json的內容才能起效,但是不當當是這樣,對應編輯器也需要安裝對應的外掛,sublime text 3需要安裝SublimeLinter、SublimeLinter-contrib-eslint外掛。對於所有規則的詳解,可以去看官網,也可以去這裡看,很詳細!
由於我們使用的是vue-loader,自然我們是希望能對.vue檔案eslint,那麼需要安裝eslint-plugin-html,在package.json中進行配置。然後對應webpack配置:

    {
        enforce: `pre`,
        test: /.vue$/,
        loader: `eslint-loader`,
        exclude: /node_modules/
    }

我們會發現webpack v1和v2之間會有一些不同,比如webpack1對於預先載入器處理的執行是這樣的,

  module: {
    preLoaders: [
      {
        test: /.js$/,
        loader: "eslint-loader"
      }
    ]
  }

更多的不同可以到中文網看,很詳細,不做擴充。

6. alias vue指向問題

    ...
    alias: {
        vue: `vue/dist/vue`
    },
    ...

Vue 最早會打包生成三個檔案,一個是 runtime only 的檔案 vue.common.js,一個是 compiler only 的檔案 compiler.js,一個是 runtime + compiler 的檔案 vue.js。
vue.js = vue.common.js + compiler.js,預設package.json的main是指向vue.common.js,而template 屬性的使用一定要用compiler.js,因此需要在alias改變vue指向

7. devServer的使用

之前的專案中使用的是用express啟動http服務,webpack-dev-middleware+webpack-hot-middleware,這裡會用到compiler+compilation,這個是webpack的編譯器和編譯過程的一些知識,也不是很懂,後續要去做做功課,應該可以加深對webpack執行機制的理解。這樣做的話,感覺複雜很多,對於webpack2.0 devServer似乎功能更強大更加完善了,所以直接使用就可以了。如下:

    devServer: {
        port: 8080, //埠號
        proxy: { //方向代理 /api/auth/ => http://api.example.dev
            `/api/auth/`: {
                target: `http://api.example.dev`,
                changeOrigin: true,
                pathRewrite: { `^/api`: `` }
            }
        },
        publicPath: config.publicPath,
        stats: { colors: true }
    }
    //changeOrigin會修改HTTP請求頭中的Host為target的域名, 這裡會被改為api.example.dev
    //pathRewrite用來改寫URL, 這裡我們把/api字首去掉,直接使用/auth/請求

webpack 2 打包實戰講解得非常好,非常棒。可以去看一下,一定會有所收穫!

8. 熱過載原理

webpack中文網,講的還算清楚,不過可能太笨,看起來還是雲裡霧裡的,似懂非懂的,補補課,好好看看。

9. localtunnel的使用

Localtunnel 是一個可以讓內網伺服器暴露到公網上的開源專案,使用可以看這裡

$ npm install -g localtunnel
$ lt --port 8080
your url is: https://uhhzexcifv.localtunnel.me

這樣的話,可以把我們的本地網站暫時性地暴露到公網,可以對網站做一些線上線下對比,詳細內容可以去了解一下localtunnel,這裡講的是通過上面配置,訪問https://uhhzexcifv.localtunnel.me,沒有達到理想效果,出現了Invalid Host header的錯誤,因為devServer缺少一個配置disableHostCheck: true,這樣的一個配置,很多文件上面都沒有說明,字面上面的意思不要去檢查Host,這樣設定,便可以繞過這一層檢驗,設定的配置項在optionsSchema.json中,issue可以看這裡

相關文章