V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
bili
V2EX  ›  分享创造

简单 vue 项目脚手架

  •  
  •   bili · 2017-10-19 17:43:52 +08:00 · 3096 次点击
    这是一个创建于 2352 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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
    }
    

    上面的代码对于每个配置项都有注释说明,这里有几点需要注意的:

    开发环境路径:http://localhost:3000/modules/foo.html

    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、以及对应生成的 js\css (名称一样)

    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/请求
    

    还可以配置host:0.0.0.0,允许同一局域网不同电脑的访问。 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 可以看这里

    10. 升级 webpack3.0

    webpack3.0 完美向下兼容,添加了些新特性,如范围提升,魔法注释 ” Magic Comments (暂时不知道怎么用),升级过程遇到Uncaught TypeError: Cannot read property 'call' of undefined的错误,最后在 HtmlWebpackPlugin 插件配置了chunksSortMode: 'dependency'解决了。

    11. 骨架屏的使用

    参考为 vue 项目添加骨架屏,所使用插件vue-skeleton-webpack-plugin,插件本身似乎存在着一些问题,如果是 eslint 规范代码,但是插件生成的代码不满足自己定义的 eslint 规范,会产生错误,同时,在'import [nameCap] from \'~/skeleton/[name]/skeleton.vue\''中插入到source尾部,会报错!

    1 条回复    2017-10-19 20:47:16 +08:00
    hlh3727138
        1
    hlh3727138  
       2017-10-19 20:47:16 +08:00
    楼主不错哦!!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1253 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 17:56 · PVG 01:56 · LAX 10:56 · JFK 13:56
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.